🏠 Домашнее задание

🎯 Разбор ДЗ: Generic Views + ModelViewSet + Soft Deletion К оглавлению урока

⚡ ДЗ в двух словах

Summary session 7 охватывает два LMS-задания из блока DRF ч.3/4.

ДЗ-1 (урок 33/34): Заменить представления задач и подзадач на Generic Views, добавить фильтрацию, поиск, сортировку, пагинацию.

ДЗ-2 (урок 35/36): CategoryViewSet с полным CRUD, кастомный метод count_tasks через @action, Soft Deletion для категорий.

Это не отдельное LMS-задание. Урок 39 — итоговое повторение DRF-блока (Уроки 33–38). В источнике (Summary session 7) разобраны два задания LMS из блока DRF. Ниже представлена их полная формулировка и пошаговое решение.

LMS-задание 1: Generic Views для Tasks и SubTasks

Описание: Используя Generic Views, заменить существующие классы представлений для задач (Tasks) и подзадач (SubTasks) на соответствующие классы для полного CRUD. Агрегирующий эндпоинт для статистики задач оставить как есть. Реализовать пагинацию, фильтрацию, поиск и сортировку для обоих наборов представлений.

Задание 1a: Tasks

  • ListCreateAPIView для создания и получения списка задач
  • RetrieveUpdateDestroyAPIView для получения, обновления и удаления задач
  • Пагинация для списка задач
  • Фильтрация по полям status и deadline
  • Поиск по полям title и description
  • Сортировка по полю created_at

Задание 1b: SubTasks — аналогично Tasks

LMS-задание 2: CategoryViewSet с Soft Deletion

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

Задание 2a: CategoryViewSet

  • Создать CategoryViewSet с использованием ModelViewSet для CRUD
  • Добавить маршрут для CategoryViewSet через Router
  • Добавить кастомный метод count_tasks через декоратор @action

Задание 2b: Soft Deletion для Category

  • Поля is_deleted (Boolean, default False) и deleted_at (DateTime, null=True)
  • Переопределить метод delete() в модели
  • Переопределить менеджер модели — get_queryset() возвращает только не удалённые

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

python -m venv venv
venv\Scripts\activate          # Windows

pip install django djangorestframework django-filter

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

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'django_filters',
    'tasks',
]

python manage.py migrate
python manage.py runserver

Пошаговое решение ДЗ-1: Generic Views для Tasks

Шаг 1: Модели Task и SubTask

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

class Task(models.Model):
    STATUS_CHOICES = [
        ('new', 'New'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ]
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='new')
    deadline = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'[{self.status}] {self.title}'

    class Meta:
        ordering = ['-created_at']

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

    def __str__(self):
        return f'[{self.status}] {self.title} (subtask of {self.task})'

    class Meta:
        ordering = ['-created_at']
python manage.py makemigrations
python manage.py migrate

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

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

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = '__all__'
        read_only_fields = ['created_at']

class SubTaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = SubTask
        fields = '__all__'
        read_only_fields = ['created_at']

Шаг 3: Пагинация

# tasks/pagination.py
from rest_framework.pagination import PageNumberPagination

class TaskPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 100

Шаг 4: Представления с Generic Views

# tasks/views.py
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from django.utils import timezone
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Task, SubTask
from .serializers import TaskSerializer, SubTaskSerializer
from .pagination import TaskPagination

class TaskListCreateView(ListCreateAPIView):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['status', 'deadline']
    search_fields = ['title', 'description']
    ordering_fields = ['created_at']
    pagination_class = TaskPagination

class TaskDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

class SubTaskListCreateView(ListCreateAPIView):
    queryset = SubTask.objects.all()
    serializer_class = SubTaskSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['status', 'deadline']
    search_fields = ['title', 'description']
    ordering_fields = ['created_at']
    pagination_class = TaskPagination

class SubTaskDetailView(RetrieveUpdateDestroyAPIView):
    queryset = SubTask.objects.all()
    serializer_class = SubTaskSerializer

# Агрегирующий эндпоинт — оставить как есть
@api_view(['GET'])
def task_stats(request):
    today = timezone.now().date()
    stats = Task.objects.aggregate(
        total=Count('id'),
        new=Count('id', filter=Q(status='new')),
        in_progress=Count('id', filter=Q(status='in_progress')),
        done=Count('id', filter=Q(status='done')),
        overdue=Count('id', filter=Q(deadline__lt=today) & ~Q(status='done')),
    )
    return Response(stats)

Шаг 5: URL-маршруты ДЗ-1

# tasks/urls.py
from django.urls import path
from .views import (
    TaskListCreateView, TaskDetailView,
    SubTaskListCreateView, SubTaskDetailView,
    task_stats,
)

urlpatterns = [
    path('tasks/', TaskListCreateView.as_view(), name='task-list-create'),
    path('tasks/<int:pk>/', TaskDetailView.as_view(), name='task-detail'),
    path('tasks/stats/', task_stats, name='task-stats'),
    path('subtasks/', SubTaskListCreateView.as_view(), name='subtask-list-create'),
    path('subtasks/<int:pk>/', SubTaskDetailView.as_view(), name='subtask-detail'),
]

Пошаговое решение ДЗ-2: CategoryViewSet + Soft Deletion

Шаг 6: Менеджер мягкого удаления

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

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

Шаг 7: Модель Category с Soft Deletion

# tasks/models.py — добавить к существующим
from django.utils import timezone
from .managers import SoftDeleteManager

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    objects = SoftDeleteManager()

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

    def __str__(self):
        return self.name
python manage.py makemigrations
python manage.py migrate

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

# tasks/serializers.py — добавить
from .models import Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name']  # is_deleted, deleted_at не показываем

Шаг 9: CategoryViewSet

# tasks/views.py — добавить
from rest_framework import viewsets
from rest_framework.decorators import action
from django.db.models import Count
from .models import Category
from .serializers import CategorySerializer

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer

    @action(detail=False, methods=['get'], url_path='count-tasks')
    def count_tasks(self, request):
        """Количество задач в каждой категории."""
        categories = Category.objects.annotate(task_count=Count('task'))
        data = [
            {'id': c.id, 'name': c.name, 'task_count': c.task_count}
            for c in categories
        ]
        return Response(data)
    # GET /categories/count-tasks/

Шаг 10: URL-маршруты ДЗ-2 через Router

# tasks/urls.py — добавить к существующим
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet

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

# Объединить с существующими urlpatterns:
urlpatterns += [path('', include(router.urls))]

# task_project/urls.py
from django.urls import path, include
urlpatterns = [
    path('api/', include('tasks.urls')),
]

Проверка в Postman / браузере

# Запустить сервер
python manage.py runserver

# ДЗ-1: Tasks с фильтрацией
# GET  http://127.0.0.1:8000/api/tasks/
# GET  http://127.0.0.1:8000/api/tasks/?status=new
# GET  http://127.0.0.1:8000/api/tasks/?search=urgent
# GET  http://127.0.0.1:8000/api/tasks/?ordering=-created_at
# GET  http://127.0.0.1:8000/api/tasks/?page=2&page_size=5
# POST http://127.0.0.1:8000/api/tasks/ Body: {"title":"New task","status":"new"}
# GET  http://127.0.0.1:8000/api/tasks/1/
# PATCH http://127.0.0.1:8000/api/tasks/1/ Body: {"status":"done"}
# DELETE http://127.0.0.1:8000/api/tasks/1/

# Статистика
# GET http://127.0.0.1:8000/api/tasks/stats/

# ДЗ-2: Categories
# GET  http://127.0.0.1:8000/api/categories/
# POST http://127.0.0.1:8000/api/categories/ Body: {"name":"Work"}
# GET  http://127.0.0.1:8000/api/categories/count-tasks/
# DELETE http://127.0.0.1:8000/api/categories/1/
# Убедитесь, что после DELETE категория не возвращается в списке

Проверка Soft Deletion в shell

python manage.py shell

from tasks.models import Category
cat = Category.objects.create(name='Test')
print(Category.objects.count())  # 1

cat.delete()
print(Category.objects.count())  # 0 — SoftDeleteManager фильтрует
print(cat.is_deleted)            # True
print(cat.deleted_at)            # datetime объект

Проверка в VS Code (F5)

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

Нажмите F5 — откроется отладчик Django. Поставьте точки останова в get_queryset() и delete() модели Category, чтобы отследить работу Soft Deletion пошагово.

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