"""Board and board-group HTTP adapters for the tasks API.""" from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.db.models import ( BooleanField, Count, DateTimeField, IntegerField, OuterRef, Prefetch, Q, Subquery, Value, ) from django.db.models.functions import Coalesce from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from communications.models import Message from employees.services.skill_progress import skill_queryset_with_rarity from tasks.access import task_board_access_q, user_can_access_task_board from tasks.checklist_template_access import can_manage_board_templates from tasks.models import ( Task, TaskAttachment, TaskBoard, TaskBoardGroupMembership, TaskBoardPlacement, TaskChecklistItem, TaskChecklistTemplateBoardEditor, TaskColumn, TaskCoverType, TaskExternalLink, TaskLinkedObject, ) from tasks.notifications import notify_board_members_added from tasks.realtime import get_task_board_recipient_ids, send_task_board_update from ..serializers import ( TaskBoardSerializer, TaskBoardSummarySerializer, create_default_columns, ) from .board_groups import TaskBoardGroupViewSet from .board_mixins import TaskBoardActionsMixin __all__ = ["TaskBoardGroupViewSet", "TaskBoardViewSet"] Employee = get_user_model() class TaskBoardViewSet(TaskBoardActionsMixin, viewsets.ModelViewSet): queryset = TaskBoard.objects.none() permission_classes = [IsAuthenticated] serializer_class = TaskBoardSerializer @action(detail=True, methods=["get", "post"], url_path="checklist-template-editors") def checklist_template_editors(self, request, pk=None): board = self.get_object() if request.method == "GET": return Response( { "board_id": board.pk, "can_manage": can_manage_board_templates(request.user, board), "is_owner": board.created_by_id == request.user.pk, "editor_ids": list( TaskChecklistTemplateBoardEditor.objects.filter(board=board) .order_by("user_id") .values_list("user_id", flat=True) ), } ) if board.created_by_id != request.user.pk: raise PermissionDenied( "Назначать редакторов шаблонов может создатель доски." ) try: editor_id = int(request.data.get("user_id")) except (ValueError, TypeError): return Response({"user_id": ["Выберите сотрудника."]}, status=400) employee = get_user_model().objects.filter(id=editor_id, is_active=True).first() if employee is None or not user_can_access_task_board(employee, board): return Response( {"user_id": ["Редактор должен быть активным участником доски."]}, status=status.HTTP_400_BAD_REQUEST, ) enabled = request.data.get("enabled") if not isinstance(enabled, bool): return Response({"enabled": ["Передайте true или false."]}, status=400) if enabled: TaskChecklistTemplateBoardEditor.objects.get_or_create( board=board, user=employee, defaults={"appointed_by": request.user}, ) else: TaskChecklistTemplateBoardEditor.objects.filter( board=board, user=employee ).delete() return Response( { "board_id": board.pk, "editor_ids": list( TaskChecklistTemplateBoardEditor.objects.filter(board=board) .order_by("user_id") .values_list("user_id", flat=True) ), } ) def get_serializer_class(self): if self.action == "list" and self.request.query_params.get("view") == "summary": return TaskBoardSummarySerializer return super().get_serializer_class() def get_queryset(self): current_placement = TaskBoardPlacement.objects.filter( user=self.request.user, board_id=OuterRef("pk"), ) queryset = ( TaskBoard.objects.filter(is_archived=False) .filter(task_board_access_q(self.request.user)) .select_related("created_by") .prefetch_related( "members", "departments", Prefetch( "user_placements", queryset=TaskBoardPlacement.objects.filter( user=self.request.user, ), to_attr="current_user_placements", ), Prefetch( "group_memberships", queryset=( TaskBoardGroupMembership.objects.filter( Q(group__user=self.request.user) | Q(group__is_shared=True), ) .exclude(group__hidden_by=self.request.user) .select_related("group") .distinct() ), to_attr="current_user_group_memberships", ), ) .annotate( current_user_is_pinned=Coalesce( Subquery( current_placement.values("is_pinned")[:1], output_field=BooleanField(), ), Value(False), ), current_user_pinned_at=Subquery( current_placement.values("pinned_at")[:1], output_field=DateTimeField(), ), current_user_notifications_enabled=Coalesce( Subquery( current_placement.values("notifications_enabled")[:1], output_field=BooleanField(), ), Value(True), ), tasks_count=Count("tasks", distinct=True), open_tasks_count=Count( "tasks", filter=Q(tasks__completed_at__isnull=True), distinct=True, ), completed_tasks_count=Count( "tasks", filter=Q(tasks__completed_at__isnull=False), distinct=True, ), ) .distinct() .order_by( "-current_user_is_pinned", "-current_user_pinned_at", "name", "id", ) ) if not ( self.action == "list" and self.request.query_params.get("view") == "summary" ): queryset = queryset.prefetch_related( Prefetch( "columns", queryset=TaskColumn.objects.annotate( tasks_count=Count("tasks", distinct=True), subcolumns_count=Count( "subcolumns", filter=Q(subcolumns__is_archived=False), distinct=True, ), ).order_by("parent_id", "position", "id"), ), Prefetch( "tasks", queryset=Task.objects.select_related( "created_by", "assignee", "column", "row", "cover", "cover__attachment", "cover__attachment__uploaded_by", "cover__comment", "cover__comment__author", "cover__external_link", "cover__external_link__created_by", "cover__linked_object", "cover__linked_object__content_type", ) .prefetch_related( "participants", "labels", Prefetch( "skills", queryset=skill_queryset_with_rarity(), ), Prefetch( "linked_objects", queryset=TaskLinkedObject.objects.only( "id", "task_id", "kind", ), ), Prefetch( "external_links", queryset=TaskExternalLink.objects.only( "id", "task_id", ), ), Prefetch( "checklist_items", queryset=TaskChecklistItem.objects.filter( task__cover__kind=TaskCoverType.CHECKLIST, ).only( "id", "task_id", "title", "is_completed", "position", ), ), Prefetch( "cover__comment__task_attachments", queryset=TaskAttachment.objects.select_related( "uploaded_by" ), ), ) .annotate( comments_count=Coalesce( Subquery( Message.objects.filter( chat__type="comments", chat__context_content_type=( ContentType.objects.get_for_model(Task) ), chat__context_object_id=OuterRef("pk"), is_deleted=False, ) .values("chat") .annotate(count=Count("id")) .values("count"), output_field=IntegerField(), ), Value(0), ), attachments_count=Coalesce( Subquery( TaskAttachment.objects.filter(task_id=OuterRef("pk")) .values("task_id") .annotate(count=Count("id")) .values("count"), output_field=IntegerField(), ), Value(0), ), checklist_total=Coalesce( Subquery( TaskChecklistItem.objects.filter(task_id=OuterRef("pk")) .values("task_id") .annotate(count=Count("id")) .values("count"), output_field=IntegerField(), ), Value(0), ), checklist_completed=Coalesce( Subquery( TaskChecklistItem.objects.filter( task_id=OuterRef("pk"), is_completed=True, ) .values("task_id") .annotate(count=Count("id")) .values("count"), output_field=IntegerField(), ), Value(0), ), ), ), ) return queryset def perform_create(self, serializer): board = serializer.save(created_by=self.request.user) create_default_columns(board) notify_board_members_added( board, self.request.user, board.members.filter(is_active=True), ) send_task_board_update(board, "created", "board", board.id) def perform_update(self, serializer): board = self.get_object() previous_recipient_ids = get_task_board_recipient_ids(board) previous_member_ids = set(board.members.values_list("id", flat=True)) board = serializer.save() added_member_ids = set(board.members.values_list("id", flat=True)) - ( previous_member_ids ) if added_member_ids: notify_board_members_added( board, self.request.user, Employee.objects.filter(id__in=added_member_ids, is_active=True), ) recipient_ids = previous_recipient_ids | get_task_board_recipient_ids(board) send_task_board_update( board, "updated", "board", board.id, recipient_ids=recipient_ids, ) def perform_destroy(self, instance): recipient_ids = get_task_board_recipient_ids(instance) board_id = instance.id # Task.column is protected so an occupied column cannot be removed on its # own. When the whole board is deleted, remove its tasks first so that # Django can cascade the remaining board structure without raising # ProtectedError. with transaction.atomic(): Task.objects.filter(board_id=board_id).delete() instance.delete() send_task_board_update( None, "deleted", "board", board_id, recipient_ids=recipient_ids, board_id=board_id, )