🏠 Домашнее задание 14 — Урок 31

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

📌 Тема: Работа с параметрами запроса и пагинацией ⏱️ Оценочное время: ~2–3 часа

⚡ Суть ДЗ 14

  • Задание 1: эндпоинт задач по дню недели (?weekday=вторник)
  • Задание 2: пагинация подзадач — max 5 на страницу, сортировка по убыванию даты
  • Задание 3: фильтрация подзадач по названию главной задачи и/или статусу

Проект «Менеджер задач» из предыдущих ДЗ. Расширяем существующие эндпоинты.

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

Домашнее задание 14: Работа с параметрами запроса и пагинацией

Задание 1

Написать, или обновить, если уже есть, эндпоинт на получение списка всех задач по дню недели.

  • Если никакой параметр запроса не передавался — по умолчанию выводить все записи.
  • Если был передан день недели (например вторник) — выводить список задач только на этот день недели.

Задание 2

Добавить пагинацию в отображение списка подзадач. На одну страницу должно отображаться не более 5 объектов. Отображение объектов должно идти в порядке убывания даты (от самого последнего добавленного объекта к самому первому).

Задание 3

Добавить или обновить, если уже есть, эндпоинт на получение списка всех подзадач по названию главной задачи и статусу подзадач.

  • Если фильтр-параметры в запросе не передавались — выводить данные по умолчанию, с учётом пагинации.
  • Если был передан фильтр-параметр названия главной задачи — выводить данные по этой главной задаче.
  • Если был передан фильтр-параметр конкретного статуса подзадачи — выводить данные по этому статусу.
  • Если были переданы оба фильтра — выводить данные в соответствии с этими фильтрами.

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

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

# Windows PowerShell
cd C:\path\to\your\project
.\venv\Scripts\Activate.ps1

# Windows CMD
.\venv\Scripts\activate.bat

2. Проверка зависимостей

pip list | findstr -i "django rest"
# Должны быть: Django 5.x, djangorestframework 3.15+

3. Структура проекта

Предполагаемая структура из предыдущих ДЗ:

task_manager/
├── manage.py
├── task_manager/          # основной проект
│   ├── settings.py
│   └── urls.py
└── tasks/                 # приложение
    ├── models.py
    ├── serializers.py
    ├── views.py
    └── urls.py

4. Проверка моделей

В проекте должны быть модели Task и SubTask. Если нет — создайте:

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

class Task(models.Model):
    STATUS_CHOICES = [
        ('new', 'Новая'),
        ('in_progress', 'В работе'),
        ('done', 'Выполнена'),
    ]
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(
        max_length=20, choices=STATUS_CHOICES, default='new'
    )
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title


class SubTask(models.Model):
    STATUS_CHOICES = [
        ('new', 'Новая'),
        ('in_progress', 'В работе'),
        ('done', 'Выполнена'),
    ]
    title = models.CharField(max_length=200)
    task = models.ForeignKey(
        Task, on_delete=models.CASCADE,
        related_name='subtasks'
    )
    status = models.CharField(
        max_length=20, choices=STATUS_CHOICES, default='new'
    )
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.task.title} — {self.title}"

5. Миграции

python manage.py makemigrations tasks
python manage.py migrate

6. Git — создание ветки

git checkout -b feature/hw14-query-params
git status

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

Задание 1: Фильтрация задач по дню недели

Шаг 1.1 — Маппинг дней недели

Django's __week_day использует нумерацию: 1=воскресенье, 2=понедельник, ..., 7=суббота. Создадим маппинг на русские названия.

# tasks/views.py
from django.db.models.functions import ExtractWeekDay
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Task, SubTask
from .serializers import TaskSerializer, SubTaskSerializer

# Маппинг: русское название -> номер дня в Django (__week_day)
WEEKDAY_MAP = {
    'понедельник': 2,
    'вторник': 3,
    'среда': 4,
    'четверг': 5,
    'пятница': 6,
    'суббота': 7,
    'воскресенье': 1,
}

Шаг 1.2 — Реализация APIView

class TaskListView(APIView):
    """Список задач с фильтрацией по дню недели."""

    def get(self, request):
        weekday = request.query_params.get('weekday')

        if weekday:
            weekday_lower = weekday.lower()
            weekday_num = WEEKDAY_MAP.get(weekday_lower)

            if weekday_num is None:
                return Response(
                    {'error': f'Неизвестный день недели: {weekday}. '
                              f'Допустимые значения: {", ".join(WEEKDAY_MAP.keys())}'},
                    status=status.HTTP_400_BAD_REQUEST
                )

            tasks = Task.objects.filter(
                due_date__week_day=weekday_num
            )
        else:
            # Параметр не передан — все записи
            tasks = Task.objects.all()

        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

Шаг 1.3 — Сериализатор (если нет)

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

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

class SubTaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = SubTask
        fields = '__all__'

Шаг 1.4 — URL-маршрут

# tasks/urls.py
from django.urls import path
from .views import TaskListView

urlpatterns = [
    path('tasks/', TaskListView.as_view(), name='task-list'),
]
# task_manager/urls.py
from django.contrib import admin
from django.urls import path, include

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

Задание 2: Пагинация подзадач (max 5, убывание даты)

Шаг 2.1 — Класс пагинации

# tasks/views.py (добавить в начало)
from rest_framework.pagination import PageNumberPagination

class SubTaskPagination(PageNumberPagination):
    page_size = 5                          # max 5 объектов на страницу
    page_size_query_param = 'page_size'    # ?page_size=N
    max_page_size = 50

Шаг 2.2 — APIView со списком подзадач

class SubTaskListView(APIView):
    """Список подзадач с пагинацией, сортировка по убыванию даты."""

    def get(self, request):
        # Сортировка по убыванию created_at (от нового к старому)
        subtasks = SubTask.objects.all().order_by('-created_at')

        # Пагинация
        paginator = SubTaskPagination()
        result_page = paginator.paginate_queryset(
            subtasks, request, view=self
        )

        if result_page is not None:
            serializer = SubTaskSerializer(result_page, many=True)
            return paginator.get_paginated_response(serializer.data)

        serializer = SubTaskSerializer(subtasks, many=True)
        return Response(serializer.data)

Шаг 2.3 — URL

# tasks/urls.py
urlpatterns = [
    path('tasks/', TaskListView.as_view(), name='task-list'),
    path('subtasks/', SubTaskListView.as_view(), name='subtask-list'),
]

Задание 3: Фильтрация подзадач по названию задачи и/или статусу

Шаг 3.1 — Расширяем SubTaskListView

class SubTaskListView(APIView):
    """
    Список подзадач: фильтрация по task__title и/или status,
    сортировка по убыванию даты, пагинация max 5.
    """

    def get(self, request):
        filters = {}

        # Фильтр по названию главной задачи
        task_title = request.query_params.get('task_title')
        if task_title:
            filters['task__title__icontains'] = task_title

        # Фильтр по статусу подзадачи
        sub_status = request.query_params.get('status')
        if sub_status:
            filters['status'] = sub_status

        # Применяем фильтры и сортируем
        subtasks = SubTask.objects.filter(**filters).order_by('-created_at')

        # Пагинация
        paginator = SubTaskPagination()
        result_page = paginator.paginate_queryset(
            subtasks, request, view=self
        )

        if result_page is not None:
            serializer = SubTaskSerializer(result_page, many=True)
            return paginator.get_paginated_response(serializer.data)

        serializer = SubTaskSerializer(subtasks, many=True)
        return Response(serializer.data)

Примеры URL-запросов (задание 3)

# Все подзадачи (с пагинацией)
GET http://127.0.0.1:8000/api/subtasks/

# По названию главной задачи
GET http://127.0.0.1:8000/api/subtasks/?task_title=Покупки

# По статусу
GET http://127.0.0.1:8000/api/subtasks/?status=done

# По обоим фильтрам
GET http://127.0.0.1:8000/api/subtasks/?task_title=Покупки&status=in_progress

# С кастомным размером страницы
GET http://127.0.0.1:8000/api/subtasks/?page=2&page_size=3

Полный views.py

# tasks/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.pagination import PageNumberPagination
from .models import Task, SubTask
from .serializers import TaskSerializer, SubTaskSerializer

WEEKDAY_MAP = {
    'понедельник': 2, 'вторник': 3, 'среда': 4,
    'четверг': 5, 'пятница': 6, 'суббота': 7, 'воскресенье': 1,
}


class SubTaskPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 50


class TaskListView(APIView):
    def get(self, request):
        weekday = request.query_params.get('weekday')
        if weekday:
            weekday_num = WEEKDAY_MAP.get(weekday.lower())
            if weekday_num is None:
                return Response(
                    {'error': f'Неизвестный день: {weekday}'},
                    status=status.HTTP_400_BAD_REQUEST
                )
            tasks = Task.objects.filter(due_date__week_day=weekday_num)
        else:
            tasks = Task.objects.all()
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)


class SubTaskListView(APIView):
    def get(self, request):
        filters = {}
        task_title = request.query_params.get('task_title')
        sub_status = request.query_params.get('status')

        if task_title:
            filters['task__title__icontains'] = task_title
        if sub_status:
            filters['status'] = sub_status

        subtasks = SubTask.objects.filter(**filters).order_by('-created_at')

        paginator = SubTaskPagination()
        result_page = paginator.paginate_queryset(
            subtasks, request, view=self
        )
        if result_page is not None:
            serializer = SubTaskSerializer(result_page, many=True)
            return paginator.get_paginated_response(serializer.data)

        serializer = SubTaskSerializer(subtasks, many=True)
        return Response(serializer.data)

Проверка в VS Code

Запуск сервера через терминал

# В терминале VS Code (Ctrl+`)
cd C:\path\to\project
.\venv\Scripts\Activate.ps1
python manage.py runserver

Сервер запустится на http://127.0.0.1:8000/.

Запуск через F5 (launch.json)

Создайте файл .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 — сервер запустится с поддержкой точек останова.

Точки останова для отладки

Поставьте breakpoint на строку tasks = Task.objects.filter(**filters) или subtasks = SubTask.objects.filter(**filters).order_by('-created_at') в views.py и выполните запрос в Postman — VS Code остановится на этой строке, можно проверить содержимое filters.

Проверка через Postman

Задание 1 — тесты

# Все задачи
GET http://127.0.0.1:8000/api/tasks/
# Ожидаем: все объекты Task

# Задачи на вторник
GET http://127.0.0.1:8000/api/tasks/?weekday=вторник
# Ожидаем: только задачи с due_date в вторник

# Неверный день недели
GET http://127.0.0.1:8000/api/tasks/?weekday=воскресенье1
# Ожидаем: 400 Bad Request с описанием ошибки

Задание 2 — тесты

# Первая страница (5 объектов, от новых к старым)
GET http://127.0.0.1:8000/api/subtasks/
# Ожидаем: count, next, previous, results (max 5)

# Вторая страница
GET http://127.0.0.1:8000/api/subtasks/?page=2
# Ожидаем: следующие 5 объектов

# Кастомный размер
GET http://127.0.0.1:8000/api/subtasks/?page_size=3

Задание 3 — тесты

# По названию задачи
GET http://127.0.0.1:8000/api/subtasks/?task_title=Покупки

# По статусу
GET http://127.0.0.1:8000/api/subtasks/?status=done

# Оба фильтра
GET http://127.0.0.1:8000/api/subtasks/?task_title=Покупки&status=in_progress

# Без фильтров (пагинация должна работать)
GET http://127.0.0.1:8000/api/subtasks/

Добавление тестовых данных через Django Shell

python manage.py shell
from tasks.models import Task, SubTask
from datetime import date

# Создаём задачи с разными днями недели
t1 = Task.objects.create(
    title='Покупки', status='new',
    due_date=date(2025, 6, 10)   # вторник
)
t2 = Task.objects.create(
    title='Работа', status='in_progress',
    due_date=date(2025, 6, 11)   # среда
)

# Создаём подзадачи
SubTask.objects.create(title='Молоко', task=t1, status='new')
SubTask.objects.create(title='Хлеб', task=t1, status='done')
SubTask.objects.create(title='Отчёт', task=t2, status='in_progress')
SubTask.objects.create(title='Презентация', task=t2, status='new')

print("Данные созданы")