380 lines
11 KiB
Python
380 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, time, timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils import timezone
|
|
from notifications.models import Notification
|
|
from notifications.signals import notify
|
|
|
|
from tasks.models import Task, TaskLinkedObject, TaskLinkedObjectKind
|
|
|
|
from .config import ActionURLs, MessageTemplates, NotificationVerbs
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def _actor_id(actor) -> int | None:
|
|
return getattr(actor, "id", None)
|
|
|
|
|
|
def _actor_name(actor) -> str:
|
|
if not actor:
|
|
return "Система"
|
|
return actor.get_full_name() or getattr(actor, "email", "") or str(actor)
|
|
|
|
|
|
def _task_data(task: Task, title: str, extra: dict | None = None) -> dict:
|
|
data = {
|
|
"title": title,
|
|
"task_id": task.id,
|
|
"task_title": task.title,
|
|
"board_id": task.board_id,
|
|
"board_name": task.board.name,
|
|
"column_id": task.column_id,
|
|
"column_name": task.column.name,
|
|
}
|
|
if extra:
|
|
data.update(extra)
|
|
return data
|
|
|
|
|
|
def _send_task_notification(
|
|
*,
|
|
recipient,
|
|
verb: str,
|
|
task: Task,
|
|
title: str,
|
|
description: str,
|
|
actor=None,
|
|
action_object=None,
|
|
extra_data: dict | None = None,
|
|
):
|
|
if not recipient or not getattr(recipient, "is_active", False):
|
|
return
|
|
if _actor_id(actor) and recipient.id == _actor_id(actor):
|
|
return
|
|
|
|
notify.send(
|
|
sender=actor,
|
|
recipient=recipient,
|
|
verb=verb,
|
|
action_object=action_object or task,
|
|
target=task,
|
|
description=description,
|
|
action_url=ActionURLs.task_detail(task),
|
|
data=_task_data(task, title, extra_data),
|
|
)
|
|
|
|
|
|
def _notify_many(
|
|
recipients,
|
|
*,
|
|
verb: str,
|
|
task: Task,
|
|
title: str,
|
|
description: str,
|
|
actor=None,
|
|
action_object=None,
|
|
extra_data: dict | None = None,
|
|
):
|
|
seen = set()
|
|
for recipient in recipients:
|
|
if not recipient or recipient.id in seen:
|
|
continue
|
|
seen.add(recipient.id)
|
|
_send_task_notification(
|
|
recipient=recipient,
|
|
verb=verb,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
action_object=action_object,
|
|
extra_data=extra_data,
|
|
)
|
|
|
|
|
|
def _linked_employee_ids(task: Task) -> set[int]:
|
|
employee_ct = ContentType.objects.get_for_model(User)
|
|
return set(
|
|
task.linked_objects.filter(
|
|
kind=TaskLinkedObjectKind.EMPLOYEE,
|
|
content_type=employee_ct,
|
|
).values_list("object_id", flat=True)
|
|
)
|
|
|
|
|
|
def _task_recipient_ids(
|
|
task: Task,
|
|
*,
|
|
include_created_by: bool = True,
|
|
include_assignee: bool = True,
|
|
include_linked_employees: bool = True,
|
|
include_commenters: bool = False,
|
|
) -> set[int]:
|
|
recipient_ids = set()
|
|
if include_created_by and task.created_by_id:
|
|
recipient_ids.add(task.created_by_id)
|
|
if include_assignee and task.assignee_id:
|
|
recipient_ids.add(task.assignee_id)
|
|
if include_linked_employees:
|
|
recipient_ids.update(_linked_employee_ids(task))
|
|
if include_commenters:
|
|
from communications import comments_helpers
|
|
|
|
recipient_ids.update(
|
|
message.author_id
|
|
for message in comments_helpers.get_comments(task)
|
|
if message.author_id
|
|
)
|
|
return recipient_ids
|
|
|
|
|
|
def _task_recipients(task: Task, **kwargs):
|
|
recipient_ids = _task_recipient_ids(task, **kwargs)
|
|
if not recipient_ids:
|
|
return []
|
|
return User.objects.filter(id__in=recipient_ids, is_active=True)
|
|
|
|
|
|
def notify_task_created(task: Task, actor=None):
|
|
if not task.assignee_id:
|
|
return
|
|
title, description = MessageTemplates.task_assigned(task, _actor_name(actor))
|
|
_send_task_notification(
|
|
recipient=task.assignee,
|
|
verb=NotificationVerbs.TASK_ASSIGNED,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
extra_data={"assignee_id": task.assignee_id},
|
|
)
|
|
|
|
|
|
def notify_task_updated(task: Task, actor, previous: dict, current: dict):
|
|
previous_assignee_id = previous.get("assignee_id")
|
|
current_assignee_id = current.get("assignee_id")
|
|
if previous_assignee_id != current_assignee_id:
|
|
title, description = (
|
|
MessageTemplates.task_assigned(task, _actor_name(actor))
|
|
if previous_assignee_id is None
|
|
else MessageTemplates.task_reassigned(task, _actor_name(actor))
|
|
)
|
|
recipients = []
|
|
if current_assignee_id:
|
|
recipients.extend(User.objects.filter(id=current_assignee_id))
|
|
if previous_assignee_id:
|
|
recipients.extend(User.objects.filter(id=previous_assignee_id))
|
|
_notify_many(
|
|
recipients,
|
|
verb=(
|
|
NotificationVerbs.TASK_ASSIGNED
|
|
if previous_assignee_id is None
|
|
else NotificationVerbs.TASK_REASSIGNED
|
|
),
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
extra_data={
|
|
"old_assignee_id": previous_assignee_id,
|
|
"assignee_id": current_assignee_id,
|
|
},
|
|
)
|
|
|
|
if previous.get("due_date") != current.get("due_date"):
|
|
title, description = MessageTemplates.task_due_date_changed(
|
|
task,
|
|
_actor_name(actor),
|
|
previous.get("due_date"),
|
|
current.get("due_date"),
|
|
)
|
|
_notify_many(
|
|
_task_recipients(task, include_commenters=True),
|
|
verb=NotificationVerbs.TASK_DUE_DATE_CHANGED,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
extra_data={
|
|
"old_due_date": previous.get("due_date"),
|
|
"due_date": current.get("due_date"),
|
|
},
|
|
)
|
|
|
|
|
|
def notify_task_moved(task: Task, actor, old_column, new_column):
|
|
if old_column.id == new_column.id:
|
|
return
|
|
if new_column.is_done and not old_column.is_done:
|
|
title, description = MessageTemplates.task_completed(
|
|
task,
|
|
_actor_name(actor),
|
|
)
|
|
verb = NotificationVerbs.TASK_COMPLETED
|
|
elif old_column.is_done and not new_column.is_done:
|
|
title, description = MessageTemplates.task_reopened(
|
|
task,
|
|
_actor_name(actor),
|
|
)
|
|
verb = NotificationVerbs.TASK_REOPENED
|
|
else:
|
|
return
|
|
|
|
_notify_many(
|
|
_task_recipients(task, include_commenters=True),
|
|
verb=verb,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
extra_data={
|
|
"from_column_id": old_column.id,
|
|
"from_column": old_column.name,
|
|
"to_column_id": new_column.id,
|
|
"to_column": new_column.name,
|
|
},
|
|
)
|
|
|
|
|
|
def notify_task_comment(task: Task, message):
|
|
actor = message.author
|
|
title, description = MessageTemplates.task_comment(
|
|
task,
|
|
_actor_name(actor),
|
|
message.content or "",
|
|
)
|
|
_notify_many(
|
|
_task_recipients(task, include_commenters=True),
|
|
verb=NotificationVerbs.TASK_COMMENT,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
action_object=message,
|
|
extra_data={"comment_id": message.id},
|
|
)
|
|
|
|
|
|
def notify_task_linked_object(link: TaskLinkedObject):
|
|
task = link.task
|
|
actor = link.created_by
|
|
object_type = link.get_kind_display()
|
|
title, description = MessageTemplates.task_linked_object_added(
|
|
task,
|
|
_actor_name(actor),
|
|
object_type,
|
|
)
|
|
recipient_ids = _task_recipient_ids(task, include_commenters=True)
|
|
if link.kind == TaskLinkedObjectKind.EMPLOYEE:
|
|
recipient_ids.add(int(link.object_id))
|
|
recipients = User.objects.filter(id__in=recipient_ids, is_active=True)
|
|
_notify_many(
|
|
recipients,
|
|
verb=NotificationVerbs.TASK_LINKED_OBJECT_ADDED,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=actor,
|
|
action_object=link,
|
|
extra_data={
|
|
"linked_object_id": link.id,
|
|
"linked_object_kind": link.kind,
|
|
"linked_object_type": object_type,
|
|
"linked_object_pk": link.object_id,
|
|
},
|
|
)
|
|
|
|
|
|
def notify_board_members_added(board, actor, members):
|
|
title, description = MessageTemplates.board_member_added(
|
|
board,
|
|
_actor_name(actor),
|
|
)
|
|
for member in members:
|
|
if not member or not getattr(member, "is_active", False):
|
|
continue
|
|
if _actor_id(actor) and member.id == _actor_id(actor):
|
|
continue
|
|
notify.send(
|
|
sender=actor,
|
|
recipient=member,
|
|
verb=NotificationVerbs.TASK_BOARD_MEMBER_ADDED,
|
|
action_object=board,
|
|
description=description,
|
|
action_url=ActionURLs.board_detail(board),
|
|
data={
|
|
"title": title,
|
|
"board_id": board.id,
|
|
"board_name": board.name,
|
|
},
|
|
)
|
|
|
|
|
|
def _already_sent_today(*, recipient_id: int, verb: str, task: Task) -> bool:
|
|
task_ct = ContentType.objects.get_for_model(Task)
|
|
today = timezone.localdate()
|
|
start = timezone.make_aware(
|
|
datetime.combine(today, time.min)
|
|
)
|
|
end = start + timedelta(days=1)
|
|
return Notification.objects.filter(
|
|
recipient_id=recipient_id,
|
|
verb=verb,
|
|
target_content_type=task_ct,
|
|
target_object_id=str(task.id),
|
|
timestamp__gte=start,
|
|
timestamp__lt=end,
|
|
).exists()
|
|
|
|
|
|
def dispatch_task_due_notifications() -> dict[str, int]:
|
|
today = timezone.localdate()
|
|
due_soon_count = 0
|
|
overdue_count = 0
|
|
queryset = (
|
|
Task.objects.filter(
|
|
due_date__isnull=False,
|
|
assignee__isnull=False,
|
|
completed_at__isnull=True,
|
|
column__is_done=False,
|
|
board__is_archived=False,
|
|
)
|
|
.select_related("board", "column", "assignee", "created_by")
|
|
.order_by("id")
|
|
)
|
|
for task in queryset:
|
|
if task.due_date < today:
|
|
verb = NotificationVerbs.TASK_OVERDUE
|
|
title, description = MessageTemplates.task_overdue(task)
|
|
elif task.due_date == today:
|
|
verb = NotificationVerbs.TASK_DUE_SOON
|
|
title, description = MessageTemplates.task_due_soon(task)
|
|
else:
|
|
continue
|
|
|
|
if _already_sent_today(
|
|
recipient_id=task.assignee_id,
|
|
verb=verb,
|
|
task=task,
|
|
):
|
|
continue
|
|
_send_task_notification(
|
|
recipient=task.assignee,
|
|
verb=verb,
|
|
task=task,
|
|
title=title,
|
|
description=description,
|
|
actor=None,
|
|
extra_data={"due_date": task.due_date.isoformat()},
|
|
)
|
|
if verb == NotificationVerbs.TASK_OVERDUE:
|
|
overdue_count += 1
|
|
else:
|
|
due_soon_count += 1
|
|
|
|
return {"due_soon": due_soon_count, "overdue": overdue_count}
|