Шаг 03. Модели Project, Task и Comment
⚡ Кратко: что делаем на этом шаге
Цель: Создать приложения apps/projects/ и apps/tasks/. Написать модели Project (FK → User-owner, M2M → User-members), Task (FK → Project, FK → User-assignee, TextChoices для статуса и приоритета, soft-delete поле), Comment (FK → Task, FK → User). Применить миграции.
- Файлы:
apps/projects/models.py,apps/tasks/models.py - Команды:
python manage.py makemigrations,python manage.py migrate - Результат: все таблицы созданы,
python manage.py check— OK
🎯 Цель этапа
Описываем всю предметную область Task Manager в виде Django-моделей. После этого шага схема БД зафиксирована и следующие шаги (Admin, Serializers, ViewSets) будут работать с этими же классами.
- Project — проект: владелец (FK→User), участники (M2M→User), активность
- Task — задача внутри проекта: FK→Project, исполнитель (FK→User), статус, приоритет, срок, soft-delete (
is_deleted) - Comment — комментарий к задаче: FK→Task, автор (FK→User), текст
После этого шага у нас будет
- Два приложения:
apps/projects/иapps/tasks/ - Все модели с правильными FK/M2M и
on_delete - TextChoices для статуса и приоритета Task
- Поле
is_deletedдля soft-delete (используем в этапе 11) - Миграции применены, таблицы в БД созданы
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
apps/projects/apps.py | Изменить | name = "apps.projects" |
apps/projects/models.py | Заполнить | Модель Project |
apps/tasks/apps.py | Изменить | name = "apps.tasks" |
apps/tasks/models.py | Заполнить | Модели Task и Comment |
config/settings.py | Изменить | Добавить "apps.projects", "apps.tasks" в INSTALLED_APPS |
apps/projects/migrations/0001_initial.py | Создаётся | Миграция projects |
apps/tasks/migrations/0001_initial.py | Создаётся | Миграция tasks |
🔨 Шаги
1. Создаём приложения projects и tasks
python manage.py startapp projects apps/projects
python manage.py startapp tasks apps/tasks
2. Правим apps.py для обоих приложений
# apps/projects/apps.py
from django.apps import AppConfig
class ProjectsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.projects"
verbose_name = "Проекты"
# apps/tasks/apps.py
from django.apps import AppConfig
class TasksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.tasks"
verbose_name = "Задачи"
3. Добавляем приложения в INSTALLED_APPS
# config/settings.py — фрагмент INSTALLED_APPS
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Сторонние
"rest_framework",
# Наши приложения
"apps.users",
"apps.projects", # ← добавляем
"apps.tasks", # ← добавляем
]
4. Модель Project
# apps/projects/models.py
from django.conf import settings
from django.db import models
class Project(models.Model):
"""
Проект — контейнер для задач.
У каждого проекта есть владелец (owner) и список участников (members).
Владелец всегда имеет доступ; участники — по приглашению.
"""
name = models.CharField("Название", max_length=200)
description = models.TextField("Описание", blank=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="Владелец",
on_delete=models.CASCADE,
related_name="owned_projects",
)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
verbose_name="Участники",
related_name="projects",
blank=True,
help_text="Пользователи, имеющие доступ к проекту.",
)
is_active = models.BooleanField(
"Активен",
default=True,
help_text="Неактивные проекты скрыты из API по умолчанию.",
)
created_at = models.DateTimeField("Создан", auto_now_add=True)
updated_at = models.DateTimeField("Обновлён", auto_now=True)
class Meta:
verbose_name = "Проект"
verbose_name_plural = "Проекты"
ordering = ["-created_at"]
def __str__(self) -> str:
return self.name
settings.AUTH_USER_MODEL (строка) или
get_user_model() в ForeignKey и ManyToManyField — не
from django.contrib.auth.models import User.
Это гарантирует совместимость с кастомной моделью (нашей apps.users.User).
5. Модели Task и Comment
# apps/tasks/models.py
from django.conf import settings
from django.db import models
class Task(models.Model):
"""
Задача внутри проекта.
Поддерживает soft-delete (is_deleted=True) вместо физического удаления.
Статус и приоритет — через TextChoices (современный Django-способ).
"""
class Status(models.TextChoices):
TODO = "todo", "К выполнению"
IN_PROGRESS = "in_progress", "В работе"
REVIEW = "review", "На проверке"
DONE = "done", "Выполнено"
CANCELLED = "cancelled", "Отменено"
class Priority(models.TextChoices):
LOW = "low", "Низкий"
MEDIUM = "medium", "Средний"
HIGH = "high", "Высокий"
CRITICAL = "critical", "Критический"
title = models.CharField("Название", max_length=300)
description = models.TextField("Описание", blank=True)
project = models.ForeignKey(
"projects.Project",
verbose_name="Проект",
on_delete=models.CASCADE,
related_name="tasks",
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="Исполнитель",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="assigned_tasks",
)
status = models.CharField(
"Статус",
max_length=20,
choices=Status.choices,
default=Status.TODO,
)
priority = models.CharField(
"Приоритет",
max_length=10,
choices=Priority.choices,
default=Priority.MEDIUM,
)
due_date = models.DateField(
"Срок выполнения",
null=True,
blank=True,
)
# Soft-delete: задачи не удаляются физически из БД
is_deleted = models.BooleanField(
"Удалена",
default=False,
help_text="Soft-delete: True = задача скрыта, но не удалена из БД.",
)
created_at = models.DateTimeField("Создана", auto_now_add=True)
updated_at = models.DateTimeField("Обновлена", auto_now=True)
class Meta:
verbose_name = "Задача"
verbose_name_plural = "Задачи"
ordering = ["-created_at"]
# Составной индекс для типичных запросов: задачи проекта по статусу
indexes = [
models.Index(fields=["project", "status"]),
models.Index(fields=["assignee", "status"]),
]
def __str__(self) -> str:
return f"[{self.get_status_display()}] {self.title}"
class Comment(models.Model):
"""
Комментарий к задаче.
Автор — FK на кастомного User. При удалении задачи — удаляются и комментарии (CASCADE).
"""
task = models.ForeignKey(
Task,
verbose_name="Задача",
on_delete=models.CASCADE,
related_name="comments",
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="Автор",
on_delete=models.CASCADE,
related_name="comments",
)
text = models.TextField("Текст комментария")
created_at = models.DateTimeField("Создан", auto_now_add=True)
updated_at = models.DateTimeField("Обновлён", auto_now=True)
class Meta:
verbose_name = "Комментарий"
verbose_name_plural = "Комментарии"
ordering = ["created_at"]
def __str__(self) -> str:
return f"Комментарий {self.author} к задаче #{self.task_id}"
"projects.Project"
(app_label.ModelName), а не через прямой импорт. Это решает проблему
циклических импортов между приложениями tasks и projects.
6. Создаём и применяем миграции
# Создаём миграции для обоих приложений
python manage.py makemigrations projects tasks
# Применяем
python manage.py migrate
Ожидаемый вывод makemigrations:
Migrations for 'projects':
apps/projects/migrations/0001_initial.py
- Create model Project
Migrations for 'tasks':
apps/tasks/migrations/0001_initial.py
- Create model Task
- Create index tasks_task_project_id_...
- Create model Comment
🧠 Объяснение логики
on_delete: CASCADE vs SET_NULL
Выбор on_delete — бизнес-решение:
- Task → Project (CASCADE): если проект удалён — все его задачи теряют смысл. Удаляем вместе.
- Task → assignee (SET_NULL): если пользователь удалён — задача остаётся, но без исполнителя (
assignee=None). Задача не должна исчезать. - Comment → Task (CASCADE): комментарии без задачи бессмысленны. Удаляем вместе с задачей.
- Project → owner (CASCADE): если владелец удалён — проект удаляется (жёсткая политика). В реальном проекте можно сделать SET_NULL + поле для нового владельца.
Soft-delete через is_deleted
Вместо физического удаления задач (DELETE из БД) мы помечаем их флагом
is_deleted=True. Это даёт:
- Возможность восстановления задачи
- Историю изменений (аудит-трейл)
- Корректную работу FK: комментарии к «удалённой» задаче не теряются
На шаге 11 мы добавим кастомный QuerySet, который автоматически фильтрует is_deleted=True.
Индексы в Meta
Два составных индекса по (project, status) и (assignee, status)
ускорят самые частые запросы API: «задачи проекта X со статусом Y» и
«задачи исполнителя Z по статусу». Django создаёт их автоматически через миграцию.
Status самое длинное значение — "in_progress" (11 символов),
у Priority — "critical" (8 символов). Мы указали
max_length=20 и max_length=10 соответственно с запасом.
✅ Проверка
1. Проверка миграций
python manage.py showmigrations projects tasks
projects
[X] 0001_initial
tasks
[X] 0001_initial
2. Проверка через Django shell
from apps.users.models import User
from apps.projects.models import Project
from apps.tasks.models import Task, Comment
# Создаём тестовые данные
user = User.objects.filter(is_superuser=True).first()
project = Project.objects.create(name="Тестовый проект", owner=user)
project.members.add(user)
print(project) # Тестовый проект
task = Task.objects.create(
title="Первая задача",
project=project,
assignee=user,
status=Task.Status.TODO,
priority=Task.Priority.HIGH,
)
print(task) # [К выполнению] Первая задача
print(task.get_status_display()) # К выполнению
print(task.get_priority_display()) # Высокий
comment = Comment.objects.create(task=task, author=user, text="Первый комментарий")
print(comment) # Комментарий user@example.com к задаче #1
# Проверяем related_name
print(project.tasks.count()) # 1
print(task.comments.count()) # 1
print(user.owned_projects.count()) # 1
print(user.assigned_tasks.count()) # 1
Диагностика: если что-то пошло не так
django.db.utils.OperationalError: no such table: projects_project— не запустилиmigrateValueError: Field 'project' expected a number but got...— передаёте объект вместо pk или наоборот; используйтеproject=project(объект) илиproject_id=1(pk)- Миграции не найдены — проверьте, что приложения добавлены в
INSTALLED_APPS
➡️ Что дальше
На следующем шаге мы регистрируем все модели в Django Admin,
настраиваем list_display, list_filter, search_fields
и добавляем TaskInline в ProjectAdmin.
- Готово: все модели, связи, индексы, миграции применены
- Далее (шаг 04):
apps/projects/admin.py,apps/tasks/admin.py— ModelAdmin + inline - После шага 04 можно управлять данными через Admin