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, )