✅ Практикум 4: разбор решений

21 задача с объяснением по источнику практикума

⚡ Решения — краткий обзор

# Задача 1
Project.objects.filter(date_of_creation__month=timezone.now().month)

# Задача 2
ProjectFile.objects.annotate(weekday=ExtractWeekDay('created_at')).filter(weekday=2)

# Задача 3
Project.objects.all().count()

# Задача 5
Project.objects.annotate(avg_tasks=Avg('tasks__id'))

# Задача 6
User.objects.annotate(count_of_tasks=Count('tasks__id')).values_list('username', 'count_of_tasks')

# Задача 8
User.objects.annotate(count_of_tasks=Count('tasks__id')).order_by('-count_of_tasks').values_list(...)

# Задача 9
Paginator(Task.objects.all(), 10).get_page(1)

# Задача 10
class AllProjectsSerializer(ModelSerializer):
    class Meta:
        model = Project
        fields = ['id', 'name']

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

Часть 1: ORM-запросы

Решение задачи 1 — Проекты за текущий месяц

from django.utils import timezone
from management_app.models.project import Project

cur_month_projects = Project.objects.filter(
    date_of_creation__month=timezone.now().month
)

if cur_month_projects:
    for proj in cur_month_projects:
        print("=" * 50)
        print(proj.name)
        print(proj.date_of_creation)
else:
    print("EMPTY DATA")
Разбор: Lookup __month извлекает месяц из DateField/DateTimeField. timezone.now().month возвращает текущий месяц как int (1–12). Django формирует SQL: WHERE EXTRACT(MONTH FROM date_of_creation) = N.

← Вернуться к заданию

Решение задачи 2 — Файлы проектов в день недели

from django.db.models.functions import ExtractWeekDay
from management_app.models.project import ProjectFile

monday_project_files = ProjectFile.objects.annotate(
    weekday=ExtractWeekDay('created_at')
).filter(
    weekday=2   # 2 = понедельник
)

if monday_project_files:
    for file_ in monday_project_files:
        print("=" * 50)
        print(file_.file_name)
        print(file_.file_path)
else:
    print("EMPTY DATA")
Разбор: annotate(weekday=ExtractWeekDay('created_at')) добавляет виртуальное поле weekday к каждому объекту. Затем фильтруем по нему. Значение 2 = понедельник (ISO: воскресенье=1, понедельник=2).

← Вернуться к заданию

Решение задачи 3 — Общее количество проектов

from management_app.models.project import Project

count_of_projects = Project.objects.all().count()
print(f"Count of projects: {count_of_projects}")
Разбор: Метод .count() делает SQL SELECT COUNT(*) — самый эффективный способ посчитать объекты. Не загружает сами объекты в память.

← Вернуться к заданию

Решение задачи 4 — Количество файлов для каждого проекта

from django.db.models import Count
from management_app.models import Project

projects = Project.objects.all()

project_file_counts = {}

for project in projects:
    file_count = project.project_files.aggregate(num_files=Count('id'))
    project_file_counts[project.name] = file_count['num_files']

for project_name, num_files in project_file_counts.items():
    print(f"Project: {project_name}, Number of files: {num_files}")
Разбор: Для каждого проекта вызываем .aggregate() на related manager project_files. aggregate() возвращает словарь — берём значение по ключу 'num_files'. Обратите внимание: это N+1 запросов. Более эффективный вариант — Project.objects.annotate(num_files=Count('project_files')).

← Вернуться к заданию

Решение задачи 5 — Среднее количество задач на каждом проекте

from django.db.models import Avg
from management_app.models import Project

average_project_tasks = Project.objects.annotate(avg_tasks=Avg('tasks__id'))

for project in average_project_tasks:
    print(f"Project: {project.name}, Average Number of Tasks: {project.avg_tasks}")
Разбор: annotate(avg_tasks=Avg('tasks__id')) вычисляет среднее значение поля id связанных задач. Поскольку id автоинкрементный, это фактически даёт среднее значение ID, а не количество. Для подсчёта количества корректнее использовать Count('tasks'), но задача из источника требует Avg.

← Вернуться к заданию

Решение задачи 6 — Количество задач для каждого пользователя

from django.db.models import Count
from django.contrib.auth.models import User

count_of_tasks_to_user = User.objects.annotate(
    count_of_tasks=Count('tasks__id')
).values_list('username', 'count_of_tasks')

print(count_of_tasks_to_user.query)  # вывести SQL для отладки

for username, count_of_tasks in count_of_tasks_to_user:
    print(f"Developer - {username}  |  Count of tasks: {count_of_tasks}")
Разбор: values_list('username', 'count_of_tasks') ограничивает SELECT только нужными полями. QuerySet теперь возвращает кортежи, а не объекты модели. .query — полезный инструмент отладки: печатает итоговый SQL-запрос.

← Вернуться к заданию

Решение задачи 7 — Сортировка задач по приоритету и дате

from management_app.models import Task

all_tasks = Task.objects.order_by(
    'priority',
    'due_date'
).values_list(
    'name', 'priority', 'due_date'
)

for name, priority, due_date in all_tasks:
    print(f"Task name - {name}  |  Priority: {priority}  |  Due date - {due_date}")
Разбор: order_by() принимает несколько полей. Сортировка идёт последовательно: сначала по priority, при равенстве — по due_date. Цепочка order_by().values_list() — эффективный способ получить только нужные поля в отсортированном порядке.

← Вернуться к заданию

Решение задачи 8 — Сортировка пользователей по количеству задач

from django.db.models import Count
from django.contrib.auth.models import User

sorted_users = User.objects.annotate(
    count_of_tasks=Count('tasks__id')
).order_by('-count_of_tasks').values_list('username', 'count_of_tasks')

print(sorted_users.query)  # SQL для отладки

for username, count_of_tasks in sorted_users:
    print(f"{username}  |  {count_of_tasks}")
Разбор: Минус перед именем поля в order_by('-count_of_tasks') задаёт обратный порядок (DESC). Комбинация annotate() + order_by() + values_list() — стандартный паттерн для "топ N пользователей по критерию".

← Вернуться к заданию

Решение задачи 9 — Пагинация для задач

from management_app.models import Task
from django.core.paginator import Paginator

tasks = Task.objects.all()

pages_paginator = Paginator(tasks, 10)  # 10 задач на страницу
page_number = 1
page_tasks = pages_paginator.get_page(page_number)

for task in page_tasks:
    print("=" * 50)
    print(task.name)
    print(task.status)
    print(task.priority)
    print(task.assignee.username)
Разбор: Paginator(queryset, per_page) принимает QuerySet и размер страницы. get_page(n) возвращает Page-объект, который можно итерировать. Paginator не загружает весь QuerySet сразу — только нужную страницу (LIMIT/OFFSET в SQL).

← Вернуться к заданию

Часть 2: Django REST Framework

Решение задачи 10 — Первый сериализатор для проектов

# management_app/serializers/projects.py
from rest_framework.serializers import ModelSerializer
from management_app.models import Project


class AllProjectsSerializer(ModelSerializer):
    class Meta:
        model = Project
        fields = ['id', 'name']
Разбор: ModelSerializer автоматически создаёт поля на основе модели. Вложенный класс Meta задаёт: model — с какой моделью работаем, fields — какие поля включить в сериализацию. fields = ['id', 'name'] означает только эти два поля, без остальных.

← Вернуться к заданию

Решение задачи 11 — View для списка всех проектов

from rest_framework.request import Request
from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import api_view

from management_app.serializers.projects import AllProjectsSerializer
from management_app.models.project import Project


@api_view(['GET',])
def get_all_projects(request: Request) -> JsonResponse:
    all_projects = Project.objects.all()

    if not all_projects.exists():
        return JsonResponse(
            [],
            status=status.HTTP_204_NO_CONTENT,
            safe=False
        )
    else:
        serialized_data = AllProjectsSerializer(
            all_projects, many=True
        )
        return JsonResponse(
            serialized_data.data,
            status=status.HTTP_200_OK,
            safe=False
        )

# —--------------------------
# management_app/urls.py
from django.urls import path
from management_app.views import get_all_projects

urlpatterns = [
    path('projects/', get_all_projects),
]

# —---------------------------
# project_management/urls.py
from django.urls import path, include

urlpatterns = [
    path('management_app/', include('management_app.urls')),
]
Разбор: @api_view(['GET']) — DRF-декоратор, ограничивает обработку только GET-запросами. many=True нужен для сериализации QuerySet (не одного объекта). safe=False в JsonResponse обязателен для возврата списка (массива JSON).

← Вернуться к заданию

Решение задачи 12 — Сериализатор AllTasksSerializer

from rest_framework.serializers import ModelSerializer
from management_app.models import Task


class AllTasksSerializer(ModelSerializer):
    class Meta:
        model = Task
        fields = ['id', 'name', 'status', 'priority']
Разбор: Аналогично AllProjectsSerializer, но для модели Task. Указываем только 4 поля для "общей" информации о задачах (детальный сериализатор будет в задаче 20).

← Вернуться к заданию

Решение задачи 13 — View для списка задач с фильтрацией

@api_view(['GET',])
def get_all_tasks(request: Request) -> JsonResponse:
    project_name = request.query_params.get('project')

    if not project_name:
        all_tasks = Task.objects.all()
    else:
        all_tasks = Task.objects.filter(project__name=project_name)

    if not all_tasks.exists():
        return JsonResponse(
            data=[],
            status=status.HTTP_204_NO_CONTENT,
            safe=False
        )

    serialized_data = AllTasksSerializer(all_tasks, many=True)

    return JsonResponse(
        data=serialized_data.data,
        status=status.HTTP_200_OK,
        safe=False
    )

# management_app/urls.py
from management_app.views import get_all_projects, get_all_tasks

urlpatterns = [
    path('projects/', get_all_projects),
    path('tasks/', get_all_tasks),  # NEW — /management_app/tasks/?project=TIGER
]
Разбор: request.query_params.get('project') читает query-параметр из URL, например ?project=TIGER. Если параметр не передан — возвращаем все задачи. Если передан — фильтруем через lookup project__name (переход по ForeignKey к полю name).

← Вернуться к заданию

Решение задачи 14 — TagsSerializer

from rest_framework import serializers
from management_app.models import Tag


class TagsSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = '__all__'
Разбор: fields = '__all__' включает все поля модели Tag. Обратите внимание на разные способы импорта: здесь from rest_framework import serializers и затем serializers.ModelSerializer, в предыдущих задачах — from rest_framework.serializers import ModelSerializer. Оба варианта корректны.

← Вернуться к заданию

Решение задачи 15 — View для всех тегов

@api_view(['GET',])
def get_all_tags(request: Request) -> JsonResponse:
    tags = Tag.objects.all()

    if not tags.exists():
        return JsonResponse(
            data=[],
            status=status.HTTP_204_NO_CONTENT,
            safe=False
        )

    serialized_data = TagsSerializer(tags, many=True)

    return JsonResponse(
        data=serialized_data.data,
        status=status.HTTP_200_OK,
        safe=False
    )

# management_app/urls.py
from management_app.views import get_all_projects, get_all_tasks, get_all_tags

urlpatterns = [
    path('projects/', get_all_projects),
    path('tasks/', get_all_tasks),
    path('tags/', get_all_tags),  # NEW
]

← Вернуться к заданию

Решение задачи 16 — View тег по id

@api_view(['GET',])
def get_tag_by_id(request: Request, tag_id: int) -> JsonResponse:
    try:
        tag = Tag.objects.get(id=tag_id)
    except Tag.DoesNotExist:
        return JsonResponse(
            data={},
            status=status.HTTP_204_NO_CONTENT
        )

    serialized_data = TagsSerializer(tag)

    return JsonResponse(
        data=serialized_data.data,
        status=status.HTTP_200_OK,
    )

# management_app/urls.py
urlpatterns = [
    path('projects/', get_all_projects),
    path('tasks/', get_all_tasks),
    path('tags/', get_all_tags),
    path('tags/<int:tag_id>/', get_tag_by_id),  # NEW
]
Разбор: Tag.objects.get(id=tag_id) — получить один объект по id. Если не найден — бросает исключение Tag.DoesNotExist. Обрабатываем его в try/except и возвращаем 204. URL-паттерн <int:tag_id> автоматически передаёт параметр в функцию.

← Вернуться к заданию

Решение задачи 17 — View обновление тега (PUT)

@api_view(['PUT',])
def update_required_tag(request: Request, tag_id: int) -> JsonResponse:
    try:
        tag = Tag.objects.get(id=tag_id)
    except Tag.DoesNotExist:
        return JsonResponse(
            {},
            status=status.HTTP_204_NO_CONTENT
        )

    validated_data = TagsSerializer(tag, request.data)

    if validated_data.is_valid(raise_exception=True):
        validated_data.save()
        return JsonResponse(
            validated_data.data,
            status=status.HTTP_200_OK,
        )

# management_app/urls.py
urlpatterns = [
    ...
    path('tags/<int:tag_id>/update/', update_required_tag),  # NEW
]
Разбор: TagsSerializer(tag, request.data) — создаём сериализатор для обновления: первый аргумент (tag) — существующий объект, второй (request.data) — новые данные. .is_valid(raise_exception=True) бросает ошибку автоматически при ошибке валидации. .save() вызывает update() (а не create()), так как передан экземпляр.
⚠️ Проверить по документации: в современном DRF рекомендуется передавать data явно: TagsSerializer(tag, data=request.data). Позиционный аргумент работает, но может быть неочевиден.

← Вернуться к заданию

Решение задачи 18 — View создание тега (POST)

@api_view(['POST',])
def create_new_tag(request: Request) -> JsonResponse:
    new_tag = TagsSerializer(data=request.data)

    if new_tag.is_valid(raise_exception=True):
        new_tag.save()
        return JsonResponse(
            new_tag.data,
            status=status.HTTP_201_CREATED
        )
    return JsonResponse(
        new_tag.errors,
        status=status.HTTP_400_BAD_REQUEST
    )

# management_app/urls.py
urlpatterns = [
    ...
    path('tags/create/', create_new_tag),  # NEW — до <int:tag_id>!
    path('tags/<int:tag_id>/', get_tag_by_id),
    ...
]
Разбор: При создании передаём только data=request.data (без экземпляра). .save() вызовет create(). Статус 201 Created — стандарт для успешного создания ресурса. Маршрут tags/create/ должен стоять до tags/<int:tag_id>/, иначе Django попытается преобразовать "create" в int.

← Вернуться к заданию

Решение задачи 19 — View удаление тега (DELETE)

@api_view(['DELETE',])
def delete_required_tag(request: Request, tag_id: int) -> JsonResponse:
    try:
        tag = Tag.objects.get(id=tag_id)
    except Tag.DoesNotExist:
        return JsonResponse(
            {},
            status=status.HTTP_204_NO_CONTENT
        )

    tag.delete()
    return JsonResponse(
        {
            'message': 'Tag deleted successfully',
        },
        status=status.HTTP_200_OK
    )

# management_app/urls.py
urlpatterns = [
    path('projects/', get_all_projects),
    path('tasks/', get_all_tasks),
    path('tags/', get_all_tags),
    path('tags/create/', create_new_tag),
    path('tags/<int:tag_id>/', get_tag_by_id),
    path('tags/<int:tag_id>/update/', update_required_tag),
    path('tags/<int:tag_id>/delete/', delete_required_tag),  # NEW
]
Разбор: tag.delete() удаляет объект из базы данных. После успешного удаления возвращаем 200 с сообщением (в источнике именно 200, хотя REST-стандарт также допускает 204 без тела).

← Вернуться к заданию

Решение задачи 20 — TaskInfoSerializer (вложенный сериализатор)

from rest_framework import serializers
from management_app.models import Task
from management_app.serializers.tags import TagsSerializer


class TaskInfoSerializer(serializers.ModelSerializer):
    tags = TagsSerializer(many=True)

    class Meta:
        model = Task
        fields = [
            'id',
            'name',
            'status',
            'priority',
            'tags',
            'project',
            'created_date',
            'due_date'
        ]
Разбор: Поле tags = TagsSerializer(many=True) переопределяет стандартное поле tags модели. Вместо списка ID тегов, в JSON будет вложенный массив объектов тега (со всеми полями из TagsSerializer). many=True нужен, так как тегов может быть несколько (ManyToMany).

← Вернуться к заданию

Решение задачи 21 — View детальная информация о задаче

@api_view(['GET',])
def get_task_by_id(request: Request, task_id: int) -> JsonResponse:
    try:
        task = Task.objects.get(id=task_id)
    except Task.DoesNotExist:
        return JsonResponse(
            {},
            status=status.HTTP_204_NO_CONTENT
        )

    serializer_data = TaskInfoSerializer(task)

    return JsonResponse(
        serializer_data.data,
        status=status.HTTP_200_OK,
    )

# management_app/urls.py — финальное состояние
from management_app.views import (
    get_all_projects, get_all_tasks, get_all_tags,
    get_tag_by_id, update_required_tag,
    create_new_tag, delete_required_tag, get_task_by_id
)

urlpatterns = [
    path('projects/', get_all_projects),
    path('tasks/', get_all_tasks),
    path('tasks/<int:task_id>/', get_task_by_id),  # NEW
    path('tags/', get_all_tags),
    path('tags/create/', create_new_tag),
    path('tags/<int:tag_id>/', get_tag_by_id),
    path('tags/<int:tag_id>/update/', update_required_tag),
    path('tags/<int:tag_id>/delete/', delete_required_tag),
]
Разбор: Используем TaskInfoSerializer (не AllTasksSerializer) — он включает вложенные теги и дополнительные поля. Обратите внимание: для одного объекта many=True не нужен — просто TaskInfoSerializer(task).

← Вернуться к заданию

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