319 lines
12 KiB
Python
319 lines
12 KiB
Python
"""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)
|