461 lines
16 KiB
Python
461 lines
16 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)
|
|
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 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="Доска по умолчанию",
|
|
)
|
|
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="Доска",
|
|
)
|
|
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"],
|
|
name="uniq_task_column_board_name",
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.board}: {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="Колонка",
|
|
)
|
|
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="Исполнитель",
|
|
)
|
|
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", "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}"
|