313 lines
12 KiB
Python
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,
|
|
)
|