Corp/backend/api/v1/tasks/viewsets/structure.py

319 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Column, row and label HTTP adapters for the tasks API."""
from django.db import transaction
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from tasks.access import task_board_access_q
from tasks.models import Task, TaskBoard, TaskColumn, TaskColumnRow, TaskLabel
from tasks.positioning import normalize_column_positions
from tasks.realtime import send_task_board_update
from ..serializers import (
TaskColumnRowSerializer,
TaskColumnSerializer,
TaskLabelSerializer,
)
class TaskColumnViewSet(viewsets.ModelViewSet):
queryset = TaskColumn.objects.none()
permission_classes = [IsAuthenticated]
serializer_class = TaskColumnSerializer
def get_queryset(self):
queryset = (
TaskColumn.objects.filter(board__is_archived=False)
.filter(
board__in=TaskBoard.objects.filter(
task_board_access_q(self.request.user)
)
)
.select_related("board")
.select_related("parent")
.annotate(
tasks_count=Count("tasks", distinct=True),
subcolumns_count=Count(
"subcolumns",
filter=Q(subcolumns__is_archived=False),
distinct=True,
),
)
.order_by("board_id", "parent_id", "position", "id")
)
board = self.request.query_params.get("board")
if board:
queryset = queryset.filter(board_id=board)
return queryset
def perform_create(self, serializer):
with transaction.atomic():
parent = serializer.validated_data.get("parent")
if parent:
parent = TaskColumn.objects.select_for_update().get(pk=parent.pk)
elif serializer.validated_data.get("is_done"):
board = serializer.validated_data["board"]
TaskColumn.objects.select_for_update().filter(
board=board,
parent__isnull=True,
is_done=True,
).update(is_done=False)
TaskColumn.objects.filter(
board=board,
parent__isnull=False,
is_done=True,
).update(is_done=False)
column = serializer.save(parent=parent)
if parent:
task_updates = {"column": column}
if column.is_done:
task_updates["completed_at"] = timezone.now()
else:
task_updates["completed_at"] = None
Task.objects.filter(column=parent).update(**task_updates)
send_task_board_update(column.board, "created", "column", column.id)
def perform_update(self, serializer):
with transaction.atomic():
current_column = TaskColumn.objects.select_for_update().get(
pk=serializer.instance.pk
)
requested_is_done = serializer.validated_data.get(
"is_done",
current_column.is_done,
)
if current_column.parent_id is None and requested_is_done:
TaskColumn.objects.filter(
board=current_column.board,
parent__isnull=True,
is_done=True,
).exclude(pk=current_column.pk).update(is_done=False)
TaskColumn.objects.filter(
board=current_column.board,
parent__isnull=False,
is_done=True,
).exclude(parent_id=current_column.pk).update(is_done=False)
column = serializer.save()
if column.parent_id is None:
column.subcolumns.update(is_done=column.is_done)
send_task_board_update(column.board, "updated", "column", column.id)
def destroy(self, request, *args, **kwargs):
column = self.get_object()
if column.subcolumns.exists():
return Response(
{"detail": "Сначала удалите подколонки этой колонки."},
status=status.HTTP_400_BAD_REQUEST,
)
if column.tasks.exists():
return Response(
{"detail": ("Переместите задачи в другую колонку перед удалением.")},
status=status.HTTP_400_BAD_REQUEST,
)
if (
column.parent_id is None
and column.board.columns.filter(
is_archived=False,
parent__isnull=True,
).count()
<= 1
):
return Response(
{"detail": "На доске должна остаться хотя бы одна колонка."},
status=status.HTTP_400_BAD_REQUEST,
)
return super().destroy(request, *args, **kwargs)
def perform_destroy(self, instance):
board = instance.board
column_id = instance.id
instance.delete()
send_task_board_update(board, "deleted", "column", column_id)
@action(detail=False, methods=["post"], url_path="reorder")
def reorder(self, request):
"""Сохраняет единый для всех участников порядок колонок доски."""
raw_board_id = request.data.get("board")
raw_column_ids = request.data.get("column_ids")
raw_parent_id = request.data.get("parent")
if not raw_board_id:
return Response(
{"board": ["Укажите доску."]},
status=status.HTTP_400_BAD_REQUEST,
)
try:
board_id = int(raw_board_id)
except (TypeError, ValueError):
return Response(
{"board": ["Идентификатор доски должен быть числом."]},
status=status.HTTP_400_BAD_REQUEST,
)
if not isinstance(raw_column_ids, list) or not raw_column_ids:
return Response(
{"column_ids": ["Передайте полный порядок активных колонок."]},
status=status.HTTP_400_BAD_REQUEST,
)
try:
column_ids = [int(column_id) for column_id in raw_column_ids]
except (TypeError, ValueError):
return Response(
{"column_ids": ["Идентификаторы колонок должны быть числами."]},
status=status.HTTP_400_BAD_REQUEST,
)
if len(column_ids) != len(set(column_ids)):
return Response(
{"column_ids": ["Колонки в списке не должны повторяться."]},
status=status.HTTP_400_BAD_REQUEST,
)
board = get_object_or_404(
TaskBoard.objects.filter(
task_board_access_q(request.user),
is_archived=False,
),
pk=board_id,
)
parent_id = None
if raw_parent_id not in (None, ""):
try:
parent_id = int(raw_parent_id)
except (TypeError, ValueError):
return Response(
{"parent": ["Идентификатор родителя должен быть числом."]},
status=status.HTTP_400_BAD_REQUEST,
)
get_object_or_404(
TaskColumn.objects.filter(
board=board,
parent__isnull=True,
is_archived=False,
),
pk=parent_id,
)
with transaction.atomic():
columns = list(
TaskColumn.objects.select_for_update()
.filter(
board=board,
parent_id=parent_id,
is_archived=False,
)
.order_by("position", "id")
)
columns_by_id = {column.id: column for column in columns}
if set(column_ids) != set(columns_by_id):
return Response(
{
"column_ids": [
"Список должен содержать все соседние активные колонки."
]
},
status=status.HTTP_400_BAD_REQUEST,
)
updated_at = timezone.now()
ordered_columns = []
for index, column_id in enumerate(column_ids, start=1):
column = columns_by_id[column_id]
column.position = index * 1000
column.updated_at = updated_at
ordered_columns.append(column)
TaskColumn.objects.bulk_update(
ordered_columns,
["position", "updated_at"],
)
send_task_board_update(
board,
"reordered",
"column",
extra={"column_ids": column_ids},
)
return Response(
{
"board": board.id,
"parent": parent_id,
"column_ids": column_ids,
}
)
class TaskColumnRowViewSet(viewsets.ModelViewSet):
queryset = TaskColumnRow.objects.none()
permission_classes = [IsAuthenticated]
serializer_class = TaskColumnRowSerializer
def get_queryset(self):
queryset = (
TaskColumnRow.objects.filter(
column__board__is_archived=False,
column__board__in=TaskBoard.objects.filter(
task_board_access_q(self.request.user)
),
)
.select_related("column", "column__board")
.annotate(tasks_count=Count("tasks", distinct=True))
.order_by("column_id", "position", "id")
)
if column := self.request.query_params.get("column"):
queryset = queryset.filter(column_id=column)
if board := self.request.query_params.get("board"):
queryset = queryset.filter(column__board_id=board)
return queryset
def perform_create(self, serializer):
row = serializer.save()
send_task_board_update(row.column.board, "created", "column_row", row.id)
def perform_update(self, serializer):
row = serializer.save()
send_task_board_update(row.column.board, "updated", "column_row", row.id)
def perform_destroy(self, instance):
board = instance.column.board
row_id = instance.id
leaf_columns = list(
instance.column.subcolumns.filter(is_archived=False).order_by(
"position",
"id",
)
) or [instance.column]
instance.delete()
for column in leaf_columns:
normalize_column_positions(column, None)
send_task_board_update(board, "deleted", "column_row", row_id)
class TaskLabelViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = TaskLabelSerializer
def get_queryset(self):
return TaskLabel.objects.order_by("name", "id")
def perform_create(self, serializer):
serializer.save()
def perform_update(self, serializer):
board_ids = list(
serializer.instance.tasks.values_list("board_id", flat=True).distinct()
)
label = serializer.save()
for board in TaskBoard.objects.filter(id__in=board_ids):
send_task_board_update(board, "updated", "label", label.id)
def perform_destroy(self, instance):
board_ids = list(instance.tasks.values_list("board_id", flat=True).distinct())
label_id = instance.id
instance.delete()
for board in TaskBoard.objects.filter(id__in=board_ids):
send_task_board_update(board, "deleted", "label", label_id)