2026-09-07 19:43:48 +03:00

313 lines
12 KiB
Python

"""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 viewsets
from rest_framework.permissions import IsAuthenticated
from communications.models import Message
from employees.services.skill_progress import skill_queryset_with_rarity
from tasks.access import task_board_access_q
from tasks.models import (
Task,
TaskAttachment,
TaskBoard,
TaskBoardGroupMembership,
TaskBoardPlacement,
TaskChecklistItem,
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
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,
)