🏠 Домашнее задание
⚡ ДЗ в двух словах
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 пошагово.
Связь с теорией
- Теория: Generic Views — все 9 классов и их назначение
- Теория: ViewSets и Router — ModelViewSet, @action, DefaultRouter
- Теория: Soft Deletion — is_deleted, SoftDeleteManager
- Теория: filter_backends — DjangoFilterBackend, SearchFilter, OrderingFilter
- Теория: Пагинация — PageNumberPagination
- Примеры — полные примеры с аннотированным кодом