Corp/backend/tasks/models.py

617 lines
21 KiB
Python

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from employees.models import Department
class TaskPriority(models.TextChoices):
LOW = "low", "Низкий"
MEDIUM = "medium", "Средний"
HIGH = "high", "Высокий"
CRITICAL = "critical", "Критический"
class TaskBoardAccessScope(models.TextChoices):
ALL = "all", "Для всех"
PRIVATE = "private", "Для себя"
RESTRICTED = "restricted", "Выборочно"
class TaskBoard(models.Model):
name = models.CharField("Название", max_length=255)
description = models.TextField("Описание", blank=True)
avatar = models.ImageField(
"Аватар доски",
upload_to="task_board_avatars/%Y/%m/",
null=True,
blank=True,
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="created_task_boards",
verbose_name="Создал",
)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="task_boards",
verbose_name="Участники",
)
departments = models.ManyToManyField(
Department,
blank=True,
related_name="task_boards",
verbose_name="Отделы",
)
access_scope = models.CharField(
"Доступ",
max_length=16,
choices=TaskBoardAccessScope.choices,
default=TaskBoardAccessScope.ALL,
)
is_archived = models.BooleanField("Архивная", default=False)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Доска задач"
verbose_name_plural = "Доски задач"
ordering = ["name", "id"]
def __str__(self):
return self.name
class TaskBoardGroup(models.Model):
name = models.CharField("Название", max_length=120)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="task_board_groups",
verbose_name="Пользователь",
)
position = models.PositiveIntegerField("Позиция", default=0)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Группа досок задач"
verbose_name_plural = "Группы досок задач"
ordering = ["position", "name", "id"]
constraints = [
models.UniqueConstraint(
fields=["user", "name"],
name="uniq_task_board_group_user_name",
)
]
def __str__(self):
return f"{self.user}: {self.name}"
class TaskBoardPlacement(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="task_board_placements",
verbose_name="Пользователь",
)
board = models.ForeignKey(
TaskBoard,
on_delete=models.CASCADE,
related_name="user_placements",
verbose_name="Доска",
)
group = models.ForeignKey(
TaskBoardGroup,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="board_placements",
verbose_name="Группа",
)
is_pinned = models.BooleanField("Закреплена", default=False)
pinned_at = models.DateTimeField("Закреплена в", null=True, blank=True)
notifications_enabled = models.BooleanField("Уведомления", default=True)
view_state = models.JSONField(
"Состояние представления",
default=dict,
blank=True,
)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Размещение доски задач"
verbose_name_plural = "Размещения досок задач"
constraints = [
models.UniqueConstraint(
fields=["user", "board"],
name="uniq_task_board_placement_user_board",
)
]
indexes = [
models.Index(fields=["user", "is_pinned"]),
models.Index(fields=["board", "user"]),
]
def __str__(self):
return f"{self.user}: {self.board} -> {self.group}"
class TaskUserSettings(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="task_settings",
verbose_name="Пользователь",
)
default_board = models.ForeignKey(
TaskBoard,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="default_for_user_settings",
verbose_name="Доска по умолчанию",
)
default_group = models.ForeignKey(
TaskBoardGroup,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="default_for_user_settings",
verbose_name="Группа досок по умолчанию",
)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Настройки задач пользователя"
verbose_name_plural = "Настройки задач пользователей"
def __str__(self):
return f"{self.user} - задачи"
class TaskColumn(models.Model):
board = models.ForeignKey(
TaskBoard,
on_delete=models.CASCADE,
related_name="columns",
verbose_name="Доска",
)
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
related_name="subcolumns",
null=True,
blank=True,
verbose_name="Родительская колонка",
)
name = models.CharField("Название", max_length=120)
position = models.PositiveIntegerField("Позиция", default=0)
color = models.CharField("Цвет", max_length=32, blank=True)
is_done = models.BooleanField("Финальная колонка", default=False)
is_archived = models.BooleanField("Архивная", default=False)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Колонка задач"
verbose_name_plural = "Колонки задач"
ordering = ["position", "id"]
constraints = [
models.UniqueConstraint(
fields=["board", "name"],
condition=models.Q(parent__isnull=True),
name="uniq_task_top_column_board_name",
),
models.UniqueConstraint(
fields=["parent", "name"],
condition=models.Q(parent__isnull=False),
name="uniq_task_subcolumn_parent_name",
),
models.UniqueConstraint(
fields=["board"],
condition=models.Q(parent__isnull=True, is_done=True),
name="uniq_task_done_column_per_board",
),
]
def __str__(self):
return f"{self.board}: {self.name}"
@property
def is_subcolumn(self):
return self.parent_id is not None
class TaskColumnRow(models.Model):
column = models.ForeignKey(
TaskColumn,
on_delete=models.CASCADE,
related_name="rows",
verbose_name="Основная колонка",
)
name = models.CharField("Название", max_length=120)
position = models.PositiveIntegerField("Позиция", default=0)
color = models.CharField("Цвет", max_length=32, blank=True)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Дорожка колонки"
verbose_name_plural = "Дорожки колонок"
ordering = ["position", "id"]
constraints = [
models.UniqueConstraint(
fields=["column", "name"],
name="uniq_task_column_row_name",
)
]
def __str__(self):
return f"{self.column}: {self.name}"
class TaskLabel(models.Model):
board = models.ForeignKey(
TaskBoard,
on_delete=models.CASCADE,
related_name="labels",
verbose_name="Доска",
)
name = models.CharField("Название", max_length=80)
color = models.CharField("Цвет", max_length=32, default="#38bdf8")
class Meta:
verbose_name = "Метка задачи"
verbose_name_plural = "Метки задач"
ordering = ["name", "id"]
constraints = [
models.UniqueConstraint(
fields=["board", "name"],
name="uniq_task_label_board_name",
)
]
def __str__(self):
return self.name
class Task(models.Model):
board = models.ForeignKey(
TaskBoard,
on_delete=models.CASCADE,
related_name="tasks",
verbose_name="Доска",
)
column = models.ForeignKey(
TaskColumn,
on_delete=models.PROTECT,
related_name="tasks",
verbose_name="Колонка",
)
row = models.ForeignKey(
TaskColumnRow,
on_delete=models.SET_NULL,
related_name="tasks",
null=True,
blank=True,
verbose_name="Дорожка",
)
title = models.CharField("Название", max_length=255)
description = models.TextField("Описание", blank=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="created_tasks",
verbose_name="Создал",
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="assigned_tasks",
verbose_name="Исполнитель",
)
participants = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="participating_tasks",
verbose_name="Участники",
)
labels = models.ManyToManyField(
TaskLabel,
blank=True,
related_name="tasks",
verbose_name="Метки",
)
priority = models.CharField(
"Приоритет",
max_length=20,
choices=TaskPriority.choices,
default=TaskPriority.MEDIUM,
)
due_date = models.DateField("Срок", null=True, blank=True)
position = models.PositiveIntegerField("Позиция", default=0)
completed_at = models.DateTimeField("Завершено", null=True, blank=True)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Задача"
verbose_name_plural = "Задачи"
ordering = ["position", "-created_at", "id"]
indexes = [
models.Index(fields=["board", "column", "row", "position"]),
models.Index(fields=["assignee", "due_date"]),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if self.column_id and self.board_id != self.column.board_id:
self.board_id = self.column.board_id
if self.column_id and self.column.is_done and not self.completed_at:
self.completed_at = timezone.now()
elif self.column_id and not self.column.is_done:
self.completed_at = None
super().save(*args, **kwargs)
class TaskAttachment(models.Model):
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="attachments",
verbose_name="Задача",
)
file = models.FileField(
"Файл",
upload_to="task_attachments/%Y/%m/%d/",
)
file_name = models.CharField("Название файла", max_length=255)
file_size = models.PositiveBigIntegerField("Размер файла (байты)")
mime_type = models.CharField("MIME-тип", max_length=255, blank=True)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="uploaded_task_attachments",
verbose_name="Загрузил",
)
created_at = models.DateTimeField("Загружено", auto_now_add=True)
class Meta:
verbose_name = "Вложение задачи"
verbose_name_plural = "Вложения задач"
ordering = ["created_at", "id"]
indexes = [models.Index(fields=["task", "created_at"])]
def __str__(self):
return f"{self.task}: {self.file_name}"
class TaskChecklistItem(models.Model):
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="checklist_items",
verbose_name="Задача",
)
title = models.CharField("Пункт", max_length=500)
position = models.PositiveIntegerField("Позиция", default=0)
is_completed = models.BooleanField("Выполнен", default=False)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="created_task_checklist_items",
verbose_name="Создал",
)
completed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="completed_task_checklist_items",
verbose_name="Отметил выполненным",
)
completed_at = models.DateTimeField("Выполнен", null=True, blank=True)
created_at = models.DateTimeField("Создано", auto_now_add=True)
updated_at = models.DateTimeField("Обновлено", auto_now=True)
class Meta:
verbose_name = "Пункт чек-листа задачи"
verbose_name_plural = "Пункты чек-листа задач"
ordering = ["position", "id"]
indexes = [
models.Index(fields=["task", "position"]),
models.Index(fields=["task", "is_completed"]),
]
def __str__(self):
return f"{self.task}: {self.title}"
class TaskLinkedObjectKind(models.TextChoices):
POST = "post", "Новость"
MESSAGE = "message", "Сообщение"
CALENDAR_EVENT = "calendar_event", "Календарное событие"
DOCUMENT = "document", "Документ"
REQUEST = "request", "Заявление"
PROCUREMENT_REQUEST = "procurement_request", "Заявка на закупку"
EMPLOYEE = "employee", "Сотрудник"
GUEST = "guest", "Гость"
GUEST_VISIT = "guest_visit", "Заявка на гостевой визит"
ATTENDANCE_RECORD = "attendance_record", "Запись посещаемости"
class TaskActivityObjectKind(models.TextChoices):
POST = "post", "Новость"
MESSAGE = "message", "Сообщение"
CALENDAR_EVENT = "calendar_event", "Календарное событие"
DOCUMENT = "document", "Документ"
REQUEST = "request", "Заявление"
PROCUREMENT_REQUEST = "procurement_request", "Заявка на закупку"
EMPLOYEE = "employee", "Сотрудник"
GUEST = "guest", "Гость"
GUEST_VISIT = "guest_visit", "Заявка на гостевой визит"
ATTENDANCE_RECORD = "attendance_record", "Запись посещаемости"
EXTERNAL_LINK = "external_link", "Внешняя ссылка"
CHECKLIST_ITEM = "checklist_item", "Пункт чек-листа"
COMMENT = "comment", "Комментарий"
class TaskActivityAction(models.TextChoices):
CREATED = "created", "Создал задачу"
UPDATED = "updated", "Обновил задачу"
CLAIMED = "claimed", "Взял задачу в работу"
MOVED = "moved", "Переместил задачу"
LINKED = "linked", "Связал объект"
UNLINKED = "unlinked", "Убрал связь"
ATTACHMENT_ADDED = "attachment_added", "Добавил файл"
ATTACHMENT_REMOVED = "attachment_removed", "Удалил файл"
CHECKLIST_ITEM_ADDED = "checklist_item_added", "Добавил пункт чек-листа"
CHECKLIST_ITEM_UPDATED = "checklist_item_updated", "Изменил пункт чек-листа"
CHECKLIST_ITEM_COMPLETED = (
"checklist_item_completed",
"Выполнил пункт чек-листа",
)
CHECKLIST_ITEM_REOPENED = (
"checklist_item_reopened",
"Вернул пункт чек-листа в работу",
)
CHECKLIST_ITEM_REMOVED = "checklist_item_removed", "Удалил пункт чек-листа"
COMMENT_ADDED = "comment_added", "Добавил комментарий"
COMMENT_EDITED = "comment_edited", "Изменил комментарий"
COMMENT_REMOVED = "comment_removed", "Удалил комментарий"
class TaskActivity(models.Model):
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="activities",
verbose_name="Задача",
)
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="task_activities",
verbose_name="Исполнитель",
)
action = models.CharField(
"Действие",
max_length=32,
choices=TaskActivityAction.choices,
)
object_kind = models.CharField(
"Тип объекта",
max_length=32,
choices=TaskActivityObjectKind.choices,
blank=True,
)
object_id = models.PositiveBigIntegerField("ID объекта", null=True, blank=True)
metadata = models.JSONField("Метаданные", default=dict, blank=True)
created_at = models.DateTimeField("Создано", auto_now_add=True)
class Meta:
verbose_name = "История задачи"
verbose_name_plural = "История задач"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["task", "-created_at"]),
]
def __str__(self):
return f"{self.task} - {self.action}"
class TaskLinkedObject(models.Model):
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="linked_objects",
verbose_name="Задача",
)
kind = models.CharField(
"Тип связи",
max_length=32,
choices=TaskLinkedObjectKind.choices,
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
verbose_name="Тип объекта",
)
object_id = models.PositiveBigIntegerField("ID объекта")
content_object = GenericForeignKey("content_type", "object_id")
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="created_task_links",
verbose_name="Связал",
)
created_at = models.DateTimeField("Создано", auto_now_add=True)
metadata = models.JSONField("Метаданные", default=dict, blank=True)
class Meta:
verbose_name = "Связанный объект задачи"
verbose_name_plural = "Связанные объекты задач"
ordering = ["-created_at", "-id"]
constraints = [
models.UniqueConstraint(
fields=["task", "content_type", "object_id"],
name="uniq_task_linked_object",
)
]
indexes = [
models.Index(fields=["task", "kind"]),
models.Index(fields=["content_type", "object_id"]),
]
def __str__(self):
return f"{self.task} -> {self.kind}:{self.object_id}"
class TaskExternalLink(models.Model):
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="external_links",
verbose_name="Задача",
)
url = models.URLField("Ссылка", max_length=2048)
title = models.CharField("Название", max_length=255, blank=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="created_task_external_links",
verbose_name="Добавил",
)
created_at = models.DateTimeField("Создано", auto_now_add=True)
class Meta:
verbose_name = "Внешняя ссылка задачи"
verbose_name_plural = "Внешние ссылки задач"
ordering = ["-created_at", "-id"]
constraints = [
models.UniqueConstraint(
fields=["task", "url"],
name="uniq_task_external_link",
)
]
indexes = [models.Index(fields=["task", "created_at"])]
def __str__(self):
return f"{self.task} -> {self.title or self.url}"