Corp/backend/tasks/realtime.py

150 lines
4.2 KiB
Python

import logging
from collections.abc import Iterable
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth import get_user_model
from django.db import transaction
from employees.models import Department, EmployeeDepartment, RoleAssignment
from .models import TaskBoard, TaskBoardAccessScope
logger = logging.getLogger(__name__)
def get_task_board_recipient_ids(board: TaskBoard) -> set[int]:
if not board or not board.pk:
return set()
User = get_user_model()
is_private = board.access_scope == TaskBoardAccessScope.PRIVATE
if (
not is_private
and board.access_scope == TaskBoardAccessScope.ALL
and not board.members.exists()
and not board.departments.exists()
):
return set(User.objects.filter(is_active=True).values_list("id", flat=True))
department_ids = (
[] if is_private else list(board.departments.values_list("id", flat=True))
)
recipient_ids = {board.created_by_id}
if not is_private:
recipient_ids.update(board.members.values_list("id", flat=True))
if department_ids:
recipient_ids.update(
EmployeeDepartment.objects.filter(
department_id__in=department_ids,
is_active=True,
employee__is_active=True,
).values_list("employee_id", flat=True)
)
recipient_ids.update(
Department.objects.filter(
id__in=department_ids,
head__is_active=True,
).values_list("head_id", flat=True)
)
recipient_ids.update(
RoleAssignment.objects.filter(
role__department_id__in=department_ids,
is_active=True,
employee__is_active=True,
).values_list("employee_id", flat=True)
)
return set(
User.objects.filter(
id__in={user_id for user_id in recipient_ids if user_id},
is_active=True,
).values_list("id", flat=True)
)
def send_task_board_update(
board: TaskBoard | None,
event: str,
model: str,
object_id: int | None = None,
*,
recipient_ids: Iterable[int] | None = None,
board_id: int | None = None,
extra: dict | None = None,
) -> None:
resolved_board_id = board_id or getattr(board, "id", None)
if not resolved_board_id:
return
recipients = set(
recipient_ids
or (get_task_board_recipient_ids(board) if board and board.pk else set())
)
if not recipients:
return
payload = {
"board_id": resolved_board_id,
"event": event,
"model": model,
"object_id": object_id,
}
if extra:
payload.update(extra)
def _send() -> None:
channel_layer = get_channel_layer()
if channel_layer is None:
return
message = {
"type": "task_board_update",
"event": event,
"data": payload,
}
for user_id in recipients:
try:
async_to_sync(channel_layer.group_send)(
f"user_{user_id}",
message,
)
except Exception as error:
logger.exception(
"Task board WebSocket update failed",
extra={
"event": "task.board.websocket_update_failed",
"user_id": user_id,
"task_board_event": event,
"error_type": type(error).__name__,
},
)
transaction.on_commit(_send)
def send_task_checklist_update(
task,
event: str,
item_id: int,
*,
cover_cleared: bool = False,
) -> None:
"""Publish checklist counters together with a checklist item change."""
extra = {
"task_id": task.id,
"checklist_total": task.checklist_items.count(),
"checklist_completed": task.checklist_items.filter(is_completed=True).count(),
}
if cover_cleared:
extra["cover"] = None
send_task_board_update(
task.board,
event,
"checklist_item",
item_id,
extra=extra,
)