🏠 Домашнее задание 16 — Урок 36

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

⚡ Суть задания

Задание 1: создать CategoryViewSet с CRUD через ModelViewSet + кастомный @action count_tasks для подсчёта задач в категории.

Задание 2: добавить в модель Category поля is_deleted и deleted_at, переопределить delete() и менеджер.

Оформление: ссылка на Git + скриншоты из Postman.

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

Домашнее задание 16: Реализация CRUD для категорий с использованием ModelViewSet, мягкое удаление

Цель: Реализовать полный CRUD для модели категорий (Categories) с помощью ModelViewSet, добавить кастомный метод для подсчёта количества задач в каждой категории. Реализовать систему мягкого удаления для категорий.

Задание 1: Реализация CRUD для категорий с использованием ModelViewSet

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

  1. Создайте CategoryViewSet, используя ModelViewSet для CRUD операций.
  2. Добавьте маршрут для CategoryViewSet.
  3. Добавьте кастомный метод count_tasks используя декоратор @action для подсчёта количества задач, связанных с каждой категорией.

Задание 2: Реализация мягкого удаления категорий

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

  1. Добавьте два новых поля в вашу модель Category, если таких ещё не было:
    • В модели Category добавьте поля is_deleted (Boolean, default False) и deleted_at (DateTime, null=true)
    • Переопределите метод удаления, чтобы он обновлял новые поля к соответствующим значениям: is_deleted=True и дата и время на момент «удаления» записи
  2. Переопределите менеджера модели Category:
    • В менеджере модели переопределите метод get_queryset(), чтобы он по умолчанию выдавал только те записи, которые не «удалены» из базы.

Оформление ответа

  1. Предоставьте решение: прикрепите ссылку на Git.
  2. Скриншоты тестирования: приложите скриншоты из браузера или Postman, подтверждающие успешное создание, обновление, получение и удаление данных через API.

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

1. Активация виртуального окружения

# Windows PowerShell
cd your_project
python -m venv venv
.\venv\Scripts\activate

# Установить зависимости (если requirements.txt есть)
pip install django djangorestframework

2. Убедиться, что DRF подключён

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'tasks',  # ваше приложение с Category и Task
]

3. Создать приложение (если ещё нет)

python manage.py startapp tasks

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

Шаг 1: Модель Category

Предполагается, что у вас уже есть модель Task с полем category (ForeignKey на Category). Добавляем поля soft deletion.

# 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)
    description = models.TextField(blank=True, default='')
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    objects = CategoryManager()       # только не удалённые
    all_objects = models.Manager()    # все (для администрирования)

    def delete(self, *args, **kwargs):
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save()

    def __str__(self):
        return self.name


class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE,
                                 null=True, blank=True,
                                 related_name='tasks')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Шаг 2: Менеджер модели

# tasks/managers.py
from django.db import models

class CategoryManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

Добавьте импорт в models.py:

from .managers import CategoryManager

Шаг 3: Сериализатор

# tasks/serializers.py
from rest_framework import serializers
from .models import Category, Task

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'description', 'is_deleted', 'deleted_at']
        read_only_fields = ['is_deleted', 'deleted_at']


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = '__all__'

Шаг 4: ViewSet с @action

# tasks/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Category
from .serializers import CategorySerializer

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()  # SoftDeleteManager: только не удалённые
    serializer_class = CategorySerializer

    @action(detail=True, methods=['get'], url_path='count-tasks')
    def count_tasks(self, request, pk=None):
        """Кастомный endpoint: GET /categories/{id}/count-tasks/"""
        category = self.get_object()
        task_count = category.tasks.count()
        return Response({
            'category': category.name,
            'task_count': task_count
        })

Шаг 5: Router и URLs

# tasks/urls.py
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet

router = DefaultRouter()
router.register(r'categories', CategoryViewSet, basename='category')

urlpatterns = router.urls
# project/urls.py
from django.contrib import admin
from django.urls import path, include

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

Шаг 6: Миграции

python manage.py makemigrations tasks
python manage.py migrate

Проверка в VS Code

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

python manage.py runserver

launch.json для F5

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

Точки останова (breakpoints)

Кликните слева от номера строки в VS Code для установки точки останова:

  • В views.py на строке category = self.get_object() — убедиться, что объект получен
  • В models.py на строке self.is_deleted = True — проверить вызов мягкого удаления
  • В managers.py на строке return super().get_queryset().filter(is_deleted=False)

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

1. Создать категорию (POST)

POST http://127.0.0.1:8000/api/categories/
Content-Type: application/json

{
    "name": "Work",
    "description": "Work-related tasks"
}

# Ответ 201:
{
    "id": 1,
    "name": "Work",
    "description": "Work-related tasks",
    "is_deleted": false,
    "deleted_at": null
}

2. Получить список (GET)

GET http://127.0.0.1:8000/api/categories/
# Возвращает только is_deleted=false

3. Обновить категорию (PATCH)

PATCH http://127.0.0.1:8000/api/categories/1/
Content-Type: application/json

{
    "description": "Updated description"
}

4. Подсчитать задачи (@action)

GET http://127.0.0.1:8000/api/categories/1/count-tasks/

# Ответ:
{
    "category": "Work",
    "task_count": 5
}

5. Мягкое удаление (DELETE)

DELETE http://127.0.0.1:8000/api/categories/1/

# Ответ: 204 No Content
# Запись в базе:
# is_deleted = True
# deleted_at = "2024-06-01T10:30:00Z"

6. Убедиться, что удалённая категория недоступна

GET http://127.0.0.1:8000/api/categories/1/
# Ответ: 404 Not Found
# (SoftDeleteManager исключает is_deleted=True)

GET http://127.0.0.1:8000/api/categories/
# Категория "Work" в списке не появляется

Связь с теорией и примерами