from django.contrib.contenttypes.models import ContentType from django.contrib.auth import get_user_model from django.db.models import ( Count, IntegerField, Max, OuterRef, Q, Subquery, Value, ) from django.db.models.functions import Coalesce from attendance.models import AttendanceRecord from communications import comments_helpers from communications.models import Message from communications.utils import user_can_access_chat from documents.models import Document from feed.models import Post from guests.models import Guest, GuestVisit from procurement.models import ProcurementRequest from requests_app.models import Request as EmployeeRequest from rest_framework import filters, status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from schedule.models import Event from tasks.access import ( task_board_access_q, user_can_access_calendar_event, user_can_access_document, user_can_access_employee, user_can_access_employee_request, user_can_access_feed_post, user_can_access_guest, user_can_access_guest_visit, user_can_access_attendance_record, user_can_access_procurement_request, ) from tasks.models import ( Task, TaskActivity, TaskActivityAction, TaskBoard, TaskColumn, TaskLabel, TaskLinkedObject, TaskLinkedObjectKind, TaskUserSettings, ) from tasks.realtime import ( get_task_board_recipient_ids, send_task_board_update, ) from .serializers import ( EmployeeBriefSerializer, TaskActivitySerializer, TaskBoardSerializer, TaskColumnSerializer, TaskLabelSerializer, TaskLinkedCalendarEventSerializer, TaskLinkedAttendanceRecordSerializer, TaskLinkedDocumentSerializer, TaskLinkedEmployeeSerializer, TaskLinkedGuestSerializer, TaskLinkedGuestVisitSerializer, TaskLinkedMessageSerializer, TaskLinkedPostSerializer, TaskLinkedProcurementRequestSerializer, TaskLinkedRequestSerializer, TaskSerializer, create_default_columns, get_attendance_record_content_type, get_calendar_event_content_type, get_document_content_type, get_employee_content_type, get_guest_content_type, get_guest_visit_content_type, get_message_content_type, get_post_content_type, get_procurement_request_content_type, get_request_content_type, ) Employee = get_user_model() def create_task_activity( task, actor, action, *, object_kind="", object_id=None, metadata=None, ): TaskActivity.objects.create( task=task, actor=actor if getattr(actor, "is_authenticated", False) else None, action=action, object_kind=object_kind or "", object_id=object_id, metadata=metadata or {}, ) def normalize_column_positions(column): task_ids = list( column.tasks.order_by("position", "-created_at", "id").values_list( "id", flat=True, ) ) for index, task_id in enumerate(task_ids): Task.objects.filter(id=task_id).update(position=(index + 1) * 1000) class TaskBoardViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = TaskBoardSerializer def get_queryset(self): return ( TaskBoard.objects.filter(is_archived=False) .filter(task_board_access_q(self.request.user)) .select_related("created_by") .prefetch_related( "members", "departments", "columns", "labels", "tasks__created_by", "tasks__assignee", "tasks__labels", "tasks__linked_objects", ) .annotate(tasks_count=Count("tasks", distinct=True)) .distinct() .order_by("name", "id") ) def perform_create(self, serializer): board = serializer.save(created_by=self.request.user) create_default_columns(board) 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) board = serializer.save() 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 instance.delete() send_task_board_update( None, "deleted", "board", board_id, recipient_ids=recipient_ids, board_id=board_id, ) @action(detail=False, methods=["get"], url_path="default") def default(self, request): queryset = self.get_queryset() settings_obj = ( TaskUserSettings.objects.select_related("default_board") .filter(user=request.user) .first() ) board = None if settings_obj and settings_obj.default_board_id: board = queryset.filter(id=settings_obj.default_board_id).first() if board is None: settings_obj.default_board = None settings_obj.save(update_fields=["default_board", "updated_at"]) if board is None: board = queryset.order_by("id").first() if not board: board = TaskBoard.objects.create( name="Рабочая доска", description="", created_by=request.user, ) create_default_columns(board) serializer = self.get_serializer(self.get_queryset().get(id=board.id)) return Response(serializer.data) @action(detail=True, methods=["post"], url_path="set-default") def set_default(self, request, pk=None): board = self.get_object() TaskUserSettings.objects.update_or_create( user=request.user, defaults={"default_board": board}, ) serializer = self.get_serializer(self.get_queryset().get(id=board.id)) return Response(serializer.data) class TaskColumnViewSet(viewsets.ModelViewSet): 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") .annotate(tasks_count=Count("tasks", distinct=True)) .order_by("board_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): column = serializer.save() send_task_board_update(column.board, "created", "column", column.id) def perform_update(self, serializer): column = serializer.save() send_task_board_update(column.board, "updated", "column", column.id) def perform_destroy(self, instance): board = instance.board column_id = instance.id instance.delete() send_task_board_update(board, "deleted", "column", column_id) class TaskLabelViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = TaskLabelSerializer def get_queryset(self): queryset = ( TaskLabel.objects.filter(board__is_archived=False) .filter( board__in=TaskBoard.objects.filter( task_board_access_q(self.request.user) ) ) .select_related("board") ) board = self.request.query_params.get("board") if board: queryset = queryset.filter(board_id=board) return queryset.order_by("name", "id") def perform_create(self, serializer): label = serializer.save() send_task_board_update(label.board, "created", "label", label.id) def perform_update(self, serializer): label = serializer.save() send_task_board_update(label.board, "updated", "label", label.id) def perform_destroy(self, instance): board = instance.board label_id = instance.id instance.delete() send_task_board_update(board, "deleted", "label", label_id) class TaskViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = TaskSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ["title", "description"] ordering_fields = ["position", "created_at", "updated_at", "due_date"] ordering = ["position", "-created_at"] def get_queryset(self): task_content_type = ContentType.objects.get_for_model(Task) comments_subquery = ( Message.objects.filter( chat__type="comments", chat__context_content_type=task_content_type, chat__context_object_id=OuterRef("pk"), is_deleted=False, ) .values("chat") .annotate(count=Count("id")) .values("count") ) queryset = ( Task.objects.filter(board__is_archived=False) .filter( board__in=TaskBoard.objects.filter( task_board_access_q(self.request.user) ) ) .select_related("board", "column", "created_by", "assignee") .prefetch_related("labels") .annotate( linked_posts_count=Count( "linked_objects", filter=Q(linked_objects__kind=TaskLinkedObjectKind.POST), distinct=True, ), linked_messages_count=Count( "linked_objects", filter=Q(linked_objects__kind=TaskLinkedObjectKind.MESSAGE), distinct=True, ), linked_events_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.CALENDAR_EVENT ), distinct=True, ), linked_documents_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.DOCUMENT, ), distinct=True, ), linked_requests_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.REQUEST, ), distinct=True, ), linked_procurement_requests_count=Count( "linked_objects", filter=Q( linked_objects__kind=( TaskLinkedObjectKind.PROCUREMENT_REQUEST ), ), distinct=True, ), linked_employees_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.EMPLOYEE, ), distinct=True, ), linked_guests_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.GUEST, ), distinct=True, ), linked_guest_visits_count=Count( "linked_objects", filter=Q( linked_objects__kind=TaskLinkedObjectKind.GUEST_VISIT, ), distinct=True, ), linked_attendance_records_count=Count( "linked_objects", filter=Q( linked_objects__kind=( TaskLinkedObjectKind.ATTENDANCE_RECORD ), ), distinct=True, ), linked_objects_count=Count("linked_objects", distinct=True), comments_count=Coalesce( Subquery(comments_subquery, output_field=IntegerField()), Value(0), ), ) ) params = self.request.query_params if board := params.get("board"): queryset = queryset.filter(board_id=board) if column := params.get("column"): queryset = queryset.filter(column_id=column) if assignee := params.get("assignee"): queryset = queryset.filter(assignee_id=assignee) if priority := params.get("priority"): queryset = queryset.filter(priority=priority) if params.get("unassigned") == "true": queryset = queryset.filter(assignee__isnull=True) return queryset def perform_create(self, serializer): task = serializer.save(created_by=self.request.user) create_task_activity( task, self.request.user, TaskActivityAction.CREATED, metadata={"column": task.column.name}, ) send_task_board_update(task.board, "created", "task", task.id) def perform_update(self, serializer): instance = serializer.instance previous = { "title": instance.title, "description": instance.description, "assignee_id": instance.assignee_id, "priority": instance.priority, "due_date": instance.due_date.isoformat() if instance.due_date else None, "label_ids": sorted(instance.labels.values_list("id", flat=True)), } task = serializer.save() current = { "title": task.title, "description": task.description, "assignee_id": task.assignee_id, "priority": task.priority, "due_date": task.due_date.isoformat() if task.due_date else None, "label_ids": sorted(task.labels.values_list("id", flat=True)), } changed_fields = [ field for field, value in current.items() if previous[field] != value ] if changed_fields: create_task_activity( task, self.request.user, TaskActivityAction.UPDATED, metadata={"fields": changed_fields}, ) send_task_board_update(task.board, "updated", "task", task.id) def perform_destroy(self, instance): board = instance.board task_id = instance.id instance.delete() send_task_board_update(board, "deleted", "task", task_id) @action(detail=True, methods=["post"]) def move(self, request, pk=None): task = self.get_object() column_id = request.data.get("column") position = request.data.get("position") if not column_id: return Response( {"column": ["Это поле обязательно."]}, status=status.HTTP_400_BAD_REQUEST, ) try: column = TaskColumn.objects.get( id=column_id, board_id=task.board_id, is_archived=False, ) except TaskColumn.DoesNotExist: return Response( {"column": ["Колонка не найдена на этой доске."]}, status=status.HTTP_400_BAD_REQUEST, ) old_column = task.column if position in (None, ""): max_position = ( Task.objects.filter(column=column).exclude(id=task.id).aggregate( Max("position") )["position__max"] or 0 ) position = max_position + 1000 else: try: position = int(position) except (TypeError, ValueError): return Response( {"position": ["Позиция должна быть числом."]}, status=status.HTTP_400_BAD_REQUEST, ) task.column = column task.position = position task.save(update_fields=["column", "position", "completed_at", "updated_at"]) normalize_column_positions(column) if old_column.id != column.id: normalize_column_positions(old_column) task.refresh_from_db() serializer = self.get_serializer(task) create_task_activity( task, request.user, TaskActivityAction.MOVED, metadata={ "from_column_id": old_column.id, "from_column": old_column.name, "to_column_id": column.id, "to_column": column.name, }, ) send_task_board_update( task.board, "moved", "task", task.id, extra={"column_id": task.column_id}, ) return Response(serializer.data) @action(detail=True, methods=["get"], url_path="activity") def activity(self, request, pk=None): task = self.get_object() queryset = task.activities.select_related("actor").order_by( "-created_at", "-id", ) serializer = TaskActivitySerializer(queryset, many=True) return Response(serializer.data) def _comment_payload(self, task, message): return { "id": message.id, "task": task.id, "author": EmployeeBriefSerializer(message.author).data, "text": message.content, "created_at": message.created_at, } @action(detail=True, methods=["get", "post"], url_path="comments") def comments(self, request, pk=None): task = self.get_object() if request.method in ("GET", "HEAD"): messages = comments_helpers.get_comments(task) return Response( [self._comment_payload(task, message) for message in messages] ) text = request.data.get("text", "").strip() if not text: return Response( {"text": ["Это поле не может быть пустым."]}, status=status.HTTP_400_BAD_REQUEST, ) message = comments_helpers.create_comment( obj=task, author=request.user, content=text, ) send_task_board_update( task.board, "commented", "task", task.id, extra={"comment_id": message.id}, ) return Response( self._comment_payload(task, message), status=status.HTTP_201_CREATED, ) @action( detail=True, methods=["delete"], url_path=r"comments/(?P\d+)", ) def delete_comment(self, request, pk=None, comment_id=None): task = self.get_object() chat = comments_helpers.get_comments_chat_if_exists(task) if chat is None: return Response( {"detail": "Комментарий не найден."}, status=status.HTTP_404_NOT_FOUND, ) try: message = Message.objects.get( id=comment_id, chat=chat, is_deleted=False, ) except Message.DoesNotExist: return Response( {"detail": "Комментарий не найден."}, status=status.HTTP_404_NOT_FOUND, ) if not ( request.user.is_staff or request.user.is_superuser or message.author_id == request.user.id ): return Response( {"detail": "Нет прав на удаление комментария."}, status=status.HTTP_403_FORBIDDEN, ) comments_helpers.delete_comment( message=message, deleted_by=request.user, soft_delete=True, ) send_task_board_update( task.board, "comment_deleted", "task", task.id, extra={"comment_id": int(comment_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=False, methods=["get"], url_path="linked-event-tasks") def linked_event_tasks(self, request): event_id = request.query_params.get("event_id") try: event_id = int(event_id) except (TypeError, ValueError): return Response( {"event_id": ["Укажите ID события."]}, status=status.HTTP_400_BAD_REQUEST, ) try: event = Event.objects.select_related( "calendar", "calendar__binding", "calendar__binding__context_content_type", "rule", "creator", ).get(id=event_id) except Event.DoesNotExist: return Response( {"event_id": ["Событие не найдено."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_calendar_event(request.user, event): return Response( {"detail": "Нет доступа к этому событию."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.CALENDAR_EVENT, linked_objects__content_type=get_calendar_event_content_type(), linked_objects__object_id=event.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-document-tasks") def linked_document_tasks(self, request): document_id = request.query_params.get("document_id") try: document_id = int(document_id) except (TypeError, ValueError): return Response( {"document_id": ["Укажите ID документа."]}, status=status.HTTP_400_BAD_REQUEST, ) try: document = ( Document.objects.select_related("uploaded_by", "file", "folder") .prefetch_related("recipients", "departments", "tags") .get(id=document_id) ) except Document.DoesNotExist: return Response( {"document_id": ["Документ не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_document(request.user, document): return Response( {"detail": "Нет доступа к этому документу."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.DOCUMENT, linked_objects__content_type=get_document_content_type(), linked_objects__object_id=document.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-request-tasks") def linked_request_tasks(self, request): request_id = request.query_params.get("request_id") try: request_id = int(request_id) except (TypeError, ValueError): return Response( {"request_id": ["Укажите ID заявления."]}, status=status.HTTP_400_BAD_REQUEST, ) try: employee_request = ( EmployeeRequest.objects.select_related( "employee", "approver", "department", ) .prefetch_related("recipients", "cc_users") .get(id=request_id) ) except EmployeeRequest.DoesNotExist: return Response( {"request_id": ["Заявление не найдено."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_employee_request(request.user, employee_request): return Response( {"detail": "Нет доступа к этому заявлению."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.REQUEST, linked_objects__content_type=get_request_content_type(), linked_objects__object_id=employee_request.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-procurement-request-tasks") def linked_procurement_request_tasks(self, request): procurement_request_id = request.query_params.get("procurement_request_id") try: procurement_request_id = int(procurement_request_id) except (TypeError, ValueError): return Response( {"procurement_request_id": ["Укажите ID заявки на закупку."]}, status=status.HTTP_400_BAD_REQUEST, ) try: procurement_request = ( ProcurementRequest.objects.select_related( "department", "processing_department", "requestor", "executor", ) .prefetch_related("approvals", "items") .get(id=procurement_request_id) ) except ProcurementRequest.DoesNotExist: return Response( {"procurement_request_id": ["Заявка на закупку не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_procurement_request(request.user, procurement_request): return Response( {"detail": "Нет доступа к этой заявке на закупку."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, linked_objects__content_type=get_procurement_request_content_type(), linked_objects__object_id=procurement_request.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-employee-tasks") def linked_employee_tasks(self, request): employee_id = request.query_params.get("employee_id") try: employee_id = int(employee_id) except (TypeError, ValueError): return Response( {"employee_id": ["Укажите ID сотрудника."]}, status=status.HTTP_400_BAD_REQUEST, ) try: employee = Employee.objects.select_related("position").get(id=employee_id) except Employee.DoesNotExist: return Response( {"employee_id": ["Сотрудник не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_employee(request.user, employee): return Response( {"detail": "Нет доступа к этому сотруднику."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.EMPLOYEE, linked_objects__content_type=get_employee_content_type(), linked_objects__object_id=employee.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-post-tasks") def linked_post_tasks(self, request): post_id = request.query_params.get("post_id") try: post_id = int(post_id) except (TypeError, ValueError): return Response( {"post_id": ["Укажите ID новости."]}, status=status.HTTP_400_BAD_REQUEST, ) try: post = Post.objects.select_related("author", "department").get( id=post_id, ) except Post.DoesNotExist: return Response( {"post_id": ["Новость не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_feed_post(request.user, post): return Response( {"detail": "Нет доступа к этой новости."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.POST, linked_objects__content_type=get_post_content_type(), linked_objects__object_id=post.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-guest-tasks") def linked_guest_tasks(self, request): guest_id = request.query_params.get("guest_id") try: guest_id = int(guest_id) except (TypeError, ValueError): return Response( {"guest_id": ["Укажите ID гостя."]}, status=status.HTTP_400_BAD_REQUEST, ) try: guest = Guest.objects.get(id=guest_id) except Guest.DoesNotExist: return Response( {"guest_id": ["Гость не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_guest(request.user, guest): return Response( {"detail": "Нет доступа к этому гостю."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.GUEST, linked_objects__content_type=get_guest_content_type(), linked_objects__object_id=guest.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-guest-visit-tasks") def linked_guest_visit_tasks(self, request): guest_visit_id = request.query_params.get("guest_visit_id") try: guest_visit_id = int(guest_visit_id) except (TypeError, ValueError): return Response( {"guest_visit_id": ["Укажите ID заявки на гостевой визит."]}, status=status.HTTP_400_BAD_REQUEST, ) try: visit = GuestVisit.objects.select_related("guest", "inviter").get( id=guest_visit_id, ) except GuestVisit.DoesNotExist: return Response( {"guest_visit_id": ["Заявка на гостевой визит не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_guest_visit(request.user, visit): return Response( {"detail": "Нет доступа к этой заявке на гостевой визит."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.GUEST_VISIT, linked_objects__content_type=get_guest_visit_content_type(), linked_objects__object_id=visit.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(detail=False, methods=["get"], url_path="linked-attendance-record-tasks") def linked_attendance_record_tasks(self, request): attendance_record_id = request.query_params.get("attendance_record_id") try: attendance_record_id = int(attendance_record_id) except (TypeError, ValueError): return Response( {"attendance_record_id": ["Укажите ID записи посещаемости."]}, status=status.HTTP_400_BAD_REQUEST, ) try: record = AttendanceRecord.objects.select_related("employee").get( id=attendance_record_id, ) except AttendanceRecord.DoesNotExist: return Response( {"attendance_record_id": ["Запись посещаемости не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_attendance_record(request.user, record): return Response( {"detail": "Нет доступа к этой записи посещаемости."}, status=status.HTTP_403_FORBIDDEN, ) queryset = ( self.get_queryset() .filter( linked_objects__kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, linked_objects__content_type=get_attendance_record_content_type(), linked_objects__object_id=record.id, ) .distinct() ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) def _message_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.MESSAGE, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _event_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.CALENDAR_EVENT, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _document_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.DOCUMENT, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _request_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.REQUEST, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _procurement_request_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _employee_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.EMPLOYEE, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _guest_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.GUEST, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _guest_visit_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.GUEST_VISIT, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _attendance_record_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) def _post_links_queryset(self, task): return ( TaskLinkedObject.objects.filter( task=task, kind=TaskLinkedObjectKind.POST, ) .select_related("created_by", "content_type") .order_by("-created_at", "-id") ) @action(detail=True, methods=["get", "post"], url_path="linked-posts") def linked_posts(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedPostSerializer( self._post_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) post_id = request.data.get("post_id") try: post_id = int(post_id) except (TypeError, ValueError): return Response( {"post_id": ["Укажите ID новости."]}, status=status.HTTP_400_BAD_REQUEST, ) try: post = Post.objects.select_related("author", "department").get( id=post_id, ) except Post.DoesNotExist: return Response( {"post_id": ["Новость не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_feed_post(request.user, post): return Response( {"detail": "Нет доступа к этой новости."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.POST, content_type=get_post_content_type(), object_id=post.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedPostSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.POST, object_id=post.id, metadata={ "object_label": post.title, "object_type": "Новость", }, ) send_task_board_update( task.board, "linked", "post", post.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-posts/(?P\d+)", ) def unlink_post(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._post_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) post_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.POST, object_id=post_id, metadata={"object_type": "Новость"}, ) send_task_board_update( task.board, "unlinked", "post", post_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-messages") def linked_messages(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedMessageSerializer( self._message_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) message_id = request.data.get("message_id") try: message_id = int(message_id) except (TypeError, ValueError): return Response( {"message_id": ["Укажите ID сообщения."]}, status=status.HTTP_400_BAD_REQUEST, ) try: message = ( Message.objects.select_related( "author", "chat", "reply_to", "reply_to__author", "poll", ) .prefetch_related( "attachments", "reactions", "reactions__user", "poll__options", ) .get(id=message_id) ) except Message.DoesNotExist: return Response( {"message_id": ["Сообщение не найдено."]}, status=status.HTTP_404_NOT_FOUND, ) if message.is_deleted: return Response( {"message_id": ["Удаленное сообщение нельзя связать с задачей."]}, status=status.HTTP_400_BAD_REQUEST, ) if not user_can_access_chat(message.chat, request.user): return Response( {"detail": "Нет доступа к этому сообщению."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.MESSAGE, content_type=get_message_content_type(), object_id=message.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedMessageSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.MESSAGE, object_id=message.id, metadata={ "object_label": message.content[:120], "object_type": "Сообщение", }, ) send_task_board_update( task.board, "linked", "message", message.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-messages/(?P\d+)", ) def unlink_message(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._message_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) message_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.MESSAGE, object_id=message_id, metadata={"object_type": "Сообщение"}, ) send_task_board_update( task.board, "unlinked", "message", message_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-documents") def linked_documents(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedDocumentSerializer( self._document_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) document_id = request.data.get("document_id") try: document_id = int(document_id) except (TypeError, ValueError): return Response( {"document_id": ["Укажите ID документа."]}, status=status.HTTP_400_BAD_REQUEST, ) try: document = ( Document.objects.select_related("uploaded_by", "file", "folder") .prefetch_related("recipients", "departments", "tags") .get(id=document_id) ) except Document.DoesNotExist: return Response( {"document_id": ["Документ не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_document(request.user, document): return Response( {"detail": "Нет доступа к этому документу."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.DOCUMENT, content_type=get_document_content_type(), object_id=document.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedDocumentSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.DOCUMENT, object_id=document.id, metadata={ "object_label": document.title, "object_type": "Документ", }, ) send_task_board_update( task.board, "linked", "document", document.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-documents/(?P\d+)", ) def unlink_document(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._document_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) document_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.DOCUMENT, object_id=document_id, metadata={"object_type": "Документ"}, ) send_task_board_update( task.board, "unlinked", "document", document_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-requests") def linked_requests(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedRequestSerializer( self._request_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) request_id = request.data.get("request_id") try: request_id = int(request_id) except (TypeError, ValueError): return Response( {"request_id": ["Укажите ID заявления."]}, status=status.HTTP_400_BAD_REQUEST, ) try: employee_request = ( EmployeeRequest.objects.select_related( "employee", "approver", "department", ) .prefetch_related("recipients", "cc_users") .get(id=request_id) ) except EmployeeRequest.DoesNotExist: return Response( {"request_id": ["Заявление не найдено."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_employee_request(request.user, employee_request): return Response( {"detail": "Нет доступа к этому заявлению."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.REQUEST, content_type=get_request_content_type(), object_id=employee_request.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedRequestSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.REQUEST, object_id=employee_request.id, metadata={ "object_label": employee_request.display_title, "object_type": "Заявление", }, ) send_task_board_update( task.board, "linked", "request", employee_request.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-requests/(?P\d+)", ) def unlink_request(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._request_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) request_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.REQUEST, object_id=request_id, metadata={"object_type": "Заявление"}, ) send_task_board_update( task.board, "unlinked", "request", request_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-procurement-requests") def linked_procurement_requests(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedProcurementRequestSerializer( self._procurement_request_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) procurement_request_id = request.data.get("procurement_request_id") try: procurement_request_id = int(procurement_request_id) except (TypeError, ValueError): return Response( {"procurement_request_id": ["Укажите ID заявки на закупку."]}, status=status.HTTP_400_BAD_REQUEST, ) try: procurement_request = ( ProcurementRequest.objects.select_related( "department", "processing_department", "requestor", "executor", ) .prefetch_related("approvals", "items") .get(id=procurement_request_id) ) except ProcurementRequest.DoesNotExist: return Response( {"procurement_request_id": ["Заявка на закупку не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_procurement_request(request.user, procurement_request): return Response( {"detail": "Нет доступа к этой заявке на закупку."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, content_type=get_procurement_request_content_type(), object_id=procurement_request.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedProcurementRequestSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, object_id=procurement_request.id, metadata={ "object_label": procurement_request.title, "object_type": "Заявка на закупку", }, ) send_task_board_update( task.board, "linked", "procurement_request", procurement_request.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-procurement-requests/(?P\d+)", ) def unlink_procurement_request(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._procurement_request_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) procurement_request_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, object_id=procurement_request_id, metadata={"object_type": "Заявка на закупку"}, ) send_task_board_update( task.board, "unlinked", "procurement_request", procurement_request_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-employees") def linked_employees(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedEmployeeSerializer( self._employee_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) employee_id = request.data.get("employee_id") try: employee_id = int(employee_id) except (TypeError, ValueError): return Response( {"employee_id": ["Укажите ID сотрудника."]}, status=status.HTTP_400_BAD_REQUEST, ) try: employee = Employee.objects.select_related("position").get(id=employee_id) except Employee.DoesNotExist: return Response( {"employee_id": ["Сотрудник не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_employee(request.user, employee): return Response( {"detail": "Нет доступа к этому сотруднику."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.EMPLOYEE, content_type=get_employee_content_type(), object_id=employee.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedEmployeeSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.EMPLOYEE, object_id=employee.id, metadata={ "object_label": employee.get_full_name() or employee.email, "object_type": "Сотрудник", }, ) send_task_board_update( task.board, "linked", "employee", employee.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-employees/(?P\d+)", ) def unlink_employee(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._employee_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) employee_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.EMPLOYEE, object_id=employee_id, metadata={"object_type": "Сотрудник"}, ) send_task_board_update( task.board, "unlinked", "employee", employee_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-guests") def linked_guests(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedGuestSerializer( self._guest_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) guest_id = request.data.get("guest_id") try: guest_id = int(guest_id) except (TypeError, ValueError): return Response( {"guest_id": ["Укажите ID гостя."]}, status=status.HTTP_400_BAD_REQUEST, ) try: guest = Guest.objects.get(id=guest_id) except Guest.DoesNotExist: return Response( {"guest_id": ["Гость не найден."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_guest(request.user, guest): return Response( {"detail": "Нет доступа к этому гостю."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.GUEST, content_type=get_guest_content_type(), object_id=guest.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedGuestSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.GUEST, object_id=guest.id, metadata={ "object_label": guest.full_name, "object_type": "Гость", }, ) send_task_board_update( task.board, "linked", "guest", guest.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-guests/(?P\d+)", ) def unlink_guest(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._guest_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) guest_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.GUEST, object_id=guest_id, metadata={"object_type": "Гость"}, ) send_task_board_update( task.board, "unlinked", "guest", guest_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-guest-visits") def linked_guest_visits(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedGuestVisitSerializer( self._guest_visit_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) guest_visit_id = request.data.get("guest_visit_id") try: guest_visit_id = int(guest_visit_id) except (TypeError, ValueError): return Response( {"guest_visit_id": ["Укажите ID заявки на гостевой визит."]}, status=status.HTTP_400_BAD_REQUEST, ) try: visit = GuestVisit.objects.select_related("guest", "inviter").get( id=guest_visit_id, ) except GuestVisit.DoesNotExist: return Response( {"guest_visit_id": ["Заявка на гостевой визит не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_guest_visit(request.user, visit): return Response( {"detail": "Нет доступа к этой заявке на гостевой визит."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.GUEST_VISIT, content_type=get_guest_visit_content_type(), object_id=visit.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedGuestVisitSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.GUEST_VISIT, object_id=visit.id, metadata={ "object_label": visit.guest.full_name, "object_type": "Заявка на гостевой визит", }, ) send_task_board_update( task.board, "linked", "guest_visit", visit.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-guest-visits/(?P\d+)", ) def unlink_guest_visit(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._guest_visit_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) guest_visit_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.GUEST_VISIT, object_id=guest_visit_id, metadata={"object_type": "Заявка на гостевой визит"}, ) send_task_board_update( task.board, "unlinked", "guest_visit", guest_visit_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-attendance-records") def linked_attendance_records(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedAttendanceRecordSerializer( self._attendance_record_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) attendance_record_id = request.data.get("attendance_record_id") try: attendance_record_id = int(attendance_record_id) except (TypeError, ValueError): return Response( {"attendance_record_id": ["Укажите ID записи посещаемости."]}, status=status.HTTP_400_BAD_REQUEST, ) try: record = AttendanceRecord.objects.select_related("employee").get( id=attendance_record_id, ) except AttendanceRecord.DoesNotExist: return Response( {"attendance_record_id": ["Запись посещаемости не найдена."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_attendance_record(request.user, record): return Response( {"detail": "Нет доступа к этой записи посещаемости."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, content_type=get_attendance_record_content_type(), object_id=record.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedAttendanceRecordSerializer( link, context={"request": request}, ) if created: employee_name = str(record.employee) if record.employee_id else "" create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, object_id=record.id, metadata={ "object_label": f"{employee_name} {record.date}".strip(), "object_type": "Запись посещаемости", }, ) send_task_board_update( task.board, "linked", "attendance_record", record.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-attendance-records/(?P\d+)", ) def unlink_attendance_record(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._attendance_record_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) attendance_record_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, object_id=attendance_record_id, metadata={"object_type": "Запись посещаемости"}, ) send_task_board_update( task.board, "unlinked", "attendance_record", attendance_record_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post"], url_path="linked-events") def linked_events(self, request, pk=None): task = self.get_object() if request.method == "GET": serializer = TaskLinkedCalendarEventSerializer( self._event_links_queryset(task), many=True, context={"request": request}, ) return Response(serializer.data) event_id = request.data.get("event_id") try: event_id = int(event_id) except (TypeError, ValueError): return Response( {"event_id": ["Укажите ID события."]}, status=status.HTTP_400_BAD_REQUEST, ) try: event = Event.objects.select_related( "calendar", "calendar__binding", "calendar__binding__context_content_type", "rule", "creator", ).get(id=event_id) except Event.DoesNotExist: return Response( {"event_id": ["Событие не найдено."]}, status=status.HTTP_404_NOT_FOUND, ) if not user_can_access_calendar_event(request.user, event): return Response( {"detail": "Нет доступа к этому событию."}, status=status.HTTP_403_FORBIDDEN, ) link, created = TaskLinkedObject.objects.get_or_create( task=task, kind=TaskLinkedObjectKind.CALENDAR_EVENT, content_type=get_calendar_event_content_type(), object_id=event.id, defaults={"created_by": request.user}, ) serializer = TaskLinkedCalendarEventSerializer( link, context={"request": request}, ) if created: create_task_activity( task, request.user, TaskActivityAction.LINKED, object_kind=TaskLinkedObjectKind.CALENDAR_EVENT, object_id=event.id, metadata={ "object_label": event.title, "object_type": "Календарное событие", }, ) send_task_board_update( task.board, "linked", "calendar_event", event.id, extra={"task_id": task.id, "link_id": link.id}, ) return Response( serializer.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, ) @action( detail=True, methods=["delete"], url_path=r"linked-events/(?P\d+)", ) def unlink_event(self, request, pk=None, link_id=None): task = self.get_object() try: link = self._event_links_queryset(task).get(id=link_id) except TaskLinkedObject.DoesNotExist: return Response( {"detail": "Связь не найдена."}, status=status.HTTP_404_NOT_FOUND, ) event_id = link.object_id link.delete() create_task_activity( task, request.user, TaskActivityAction.UNLINKED, object_kind=TaskLinkedObjectKind.CALENDAR_EVENT, object_id=event_id, metadata={"object_type": "Календарное событие"}, ) send_task_board_update( task.board, "unlinked", "calendar_event", event_id, extra={"task_id": task.id, "link_id": int(link_id)}, ) return Response(status=status.HTTP_204_NO_CONTENT)