"""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, ).distinct(), 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)