2722 lines
94 KiB
Python
2722 lines
94 KiB
Python
import mimetypes
|
||
from pathlib import Path
|
||
from urllib.parse import urlsplit
|
||
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.contrib.auth import get_user_model
|
||
from django.db import transaction
|
||
from django.db.models import (
|
||
Count,
|
||
IntegerField,
|
||
Max,
|
||
OuterRef,
|
||
Q,
|
||
Subquery,
|
||
Value,
|
||
)
|
||
from django.db.models.functions import Coalesce
|
||
from django.http import FileResponse
|
||
from django.shortcuts import get_object_or_404
|
||
from django.utils import timezone
|
||
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, parsers, 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,
|
||
TaskActivityObjectKind,
|
||
TaskAttachment,
|
||
TaskBoard,
|
||
TaskColumn,
|
||
TaskExternalLink,
|
||
TaskLabel,
|
||
TaskLinkedObject,
|
||
TaskLinkedObjectKind,
|
||
TaskUserSettings,
|
||
)
|
||
from tasks.notifications import (
|
||
notify_board_members_added,
|
||
notify_task_comment,
|
||
notify_task_created,
|
||
notify_task_moved,
|
||
notify_task_updated,
|
||
)
|
||
from tasks.realtime import (
|
||
get_task_board_recipient_ids,
|
||
send_task_board_update,
|
||
)
|
||
|
||
from .serializers import (
|
||
EmployeeBriefSerializer,
|
||
TaskActivitySerializer,
|
||
TaskAttachmentSerializer,
|
||
TaskBoardSerializer,
|
||
TaskChecklistItemSerializer,
|
||
TaskColumnSerializer,
|
||
TaskExternalLinkSerializer,
|
||
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)
|
||
|
||
|
||
def send_task_checklist_update(task, event, item_id):
|
||
send_task_board_update(
|
||
task.board,
|
||
event,
|
||
"checklist_item",
|
||
item_id,
|
||
extra={
|
||
"task_id": task.id,
|
||
"checklist_total": task.checklist_items.count(),
|
||
"checklist_completed": task.checklist_items.filter(
|
||
is_completed=True
|
||
).count(),
|
||
},
|
||
)
|
||
|
||
|
||
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",
|
||
"tasks__external_links",
|
||
"tasks__attachments",
|
||
"tasks__checklist_items",
|
||
)
|
||
.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)
|
||
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
|
||
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", "checklist_items")
|
||
.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)
|
||
+ Count("external_links", distinct=True)
|
||
),
|
||
comments_count=Coalesce(
|
||
Subquery(comments_subquery, output_field=IntegerField()),
|
||
Value(0),
|
||
),
|
||
attachments_count=Count("attachments", distinct=True),
|
||
checklist_total=Count("checklist_items", distinct=True),
|
||
checklist_completed=Count(
|
||
"checklist_items",
|
||
filter=Q(checklist_items__is_completed=True),
|
||
distinct=True,
|
||
),
|
||
)
|
||
)
|
||
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},
|
||
)
|
||
notify_task_created(task, self.request.user)
|
||
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},
|
||
)
|
||
notify_task_updated(task, self.request.user, previous, current)
|
||
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)
|
||
|
||
def _move_task(self, task, column, position, actor):
|
||
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
|
||
|
||
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()
|
||
|
||
create_task_activity(
|
||
task,
|
||
actor,
|
||
TaskActivityAction.MOVED,
|
||
metadata={
|
||
"from_column_id": old_column.id,
|
||
"from_column": old_column.name,
|
||
"to_column_id": column.id,
|
||
"to_column": column.name,
|
||
},
|
||
)
|
||
notify_task_moved(task, actor, old_column, column)
|
||
send_task_board_update(
|
||
task.board,
|
||
"moved",
|
||
"task",
|
||
task.id,
|
||
extra={"column_id": task.column_id},
|
||
)
|
||
return task
|
||
|
||
@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,
|
||
)
|
||
|
||
if position in (None, ""):
|
||
position = None
|
||
else:
|
||
try:
|
||
position = int(position)
|
||
except (TypeError, ValueError):
|
||
return Response(
|
||
{"position": ["Позиция должна быть числом."]},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
task = self._move_task(task, column, position, request.user)
|
||
serializer = self.get_serializer(task)
|
||
return Response(serializer.data)
|
||
|
||
@action(detail=True, methods=["post"])
|
||
def claim(self, request, pk=None):
|
||
visible_task = self.get_object()
|
||
with transaction.atomic():
|
||
task = (
|
||
Task.objects.select_for_update(of=("self",))
|
||
.select_related("board", "column", "assignee")
|
||
.get(id=visible_task.id)
|
||
)
|
||
|
||
if task.completed_at or task.column.is_done:
|
||
return Response(
|
||
{"detail": "Нельзя взять в работу завершённую задачу."},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
if task.assignee_id:
|
||
if task.assignee_id == request.user.id:
|
||
return Response(self.get_serializer(task).data)
|
||
return Response(
|
||
{"detail": "У задачи уже есть исполнитель."},
|
||
status=status.HTTP_409_CONFLICT,
|
||
)
|
||
|
||
task.assignee = request.user
|
||
task.save(update_fields=["assignee", "updated_at"])
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.CLAIMED,
|
||
metadata={"assignee_id": request.user.id},
|
||
)
|
||
notify_task_updated(
|
||
task,
|
||
request.user,
|
||
{"assignee_id": None},
|
||
{"assignee_id": request.user.id},
|
||
)
|
||
send_task_board_update(
|
||
task.board,
|
||
"claimed",
|
||
"task",
|
||
task.id,
|
||
extra={"assignee_id": request.user.id},
|
||
)
|
||
|
||
return Response(self.get_serializer(task).data)
|
||
|
||
@action(detail=True, methods=["post"])
|
||
def complete(self, request, pk=None):
|
||
task = self.get_object()
|
||
if task.assignee_id != request.user.id:
|
||
return Response(
|
||
{"detail": "Завершить задачу может только назначенный исполнитель."},
|
||
status=status.HTTP_403_FORBIDDEN,
|
||
)
|
||
|
||
if task.column.is_done:
|
||
return Response(self.get_serializer(task).data)
|
||
|
||
done_column = (
|
||
TaskColumn.objects.filter(
|
||
board_id=task.board_id,
|
||
is_done=True,
|
||
is_archived=False,
|
||
)
|
||
.order_by("position", "id")
|
||
.first()
|
||
)
|
||
if done_column is None:
|
||
return Response(
|
||
{"detail": "На доске не настроена завершающая колонка."},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
task = self._move_task(task, done_column, None, request.user)
|
||
return Response(self.get_serializer(task).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)
|
||
|
||
@action(
|
||
detail=True,
|
||
methods=["get", "post"],
|
||
url_path="attachments",
|
||
parser_classes=[parsers.MultiPartParser, parsers.FormParser],
|
||
)
|
||
def attachments(self, request, pk=None):
|
||
task = self.get_object()
|
||
if request.method in ("GET", "HEAD"):
|
||
queryset = task.attachments.select_related("uploaded_by")
|
||
serializer = TaskAttachmentSerializer(
|
||
queryset,
|
||
many=True,
|
||
context=self.get_serializer_context(),
|
||
)
|
||
return Response(serializer.data)
|
||
|
||
files = request.FILES.getlist("files")
|
||
if not files and request.FILES.get("file"):
|
||
files = [request.FILES["file"]]
|
||
if not files:
|
||
return Response(
|
||
{"files": ["Выберите хотя бы один файл."]},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
created = []
|
||
for uploaded in files:
|
||
file_name = Path(uploaded.name.replace("\\", "/")).name[:255]
|
||
attachment = TaskAttachment.objects.create(
|
||
task=task,
|
||
file=uploaded,
|
||
file_name=file_name,
|
||
file_size=uploaded.size,
|
||
mime_type=(uploaded.content_type or "")[:255],
|
||
uploaded_by=request.user,
|
||
)
|
||
created.append(attachment)
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.ATTACHMENT_ADDED,
|
||
object_id=attachment.id,
|
||
metadata={"file_name": attachment.file_name},
|
||
)
|
||
|
||
send_task_board_update(
|
||
task.board,
|
||
"attachments_updated",
|
||
"task",
|
||
task.id,
|
||
extra={"attachments_count": task.attachments.count()},
|
||
)
|
||
serializer = TaskAttachmentSerializer(
|
||
created,
|
||
many=True,
|
||
context=self.get_serializer_context(),
|
||
)
|
||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||
|
||
@action(
|
||
detail=True,
|
||
methods=["get"],
|
||
url_path=r"attachments/(?P<attachment_id>\d+)/download",
|
||
url_name="attachment-download",
|
||
)
|
||
def attachment_download(self, request, pk=None, attachment_id=None):
|
||
task = self.get_object()
|
||
attachment = get_object_or_404(
|
||
task.attachments,
|
||
id=attachment_id,
|
||
)
|
||
try:
|
||
file_handle = attachment.file.open("rb")
|
||
except (FileNotFoundError, OSError):
|
||
return Response(
|
||
{"detail": "Файл не найден в хранилище."},
|
||
status=status.HTTP_404_NOT_FOUND,
|
||
)
|
||
|
||
content_type = attachment.mime_type or (
|
||
mimetypes.guess_type(attachment.file_name)[0]
|
||
or "application/octet-stream"
|
||
)
|
||
response = FileResponse(
|
||
file_handle,
|
||
as_attachment=True,
|
||
filename=attachment.file_name,
|
||
content_type=content_type,
|
||
)
|
||
response["X-Content-Type-Options"] = "nosniff"
|
||
return response
|
||
|
||
@action(
|
||
detail=True,
|
||
methods=["delete"],
|
||
url_path=r"attachments/(?P<attachment_id>\d+)",
|
||
url_name="attachment-detail",
|
||
)
|
||
def attachment_detail(self, request, pk=None, attachment_id=None):
|
||
task = self.get_object()
|
||
attachment = get_object_or_404(
|
||
task.attachments,
|
||
id=attachment_id,
|
||
)
|
||
attachment_id = attachment.id
|
||
file_name = attachment.file_name
|
||
attachment.delete()
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.ATTACHMENT_REMOVED,
|
||
object_id=attachment_id,
|
||
metadata={"file_name": file_name},
|
||
)
|
||
send_task_board_update(
|
||
task.board,
|
||
"attachments_updated",
|
||
"task",
|
||
task.id,
|
||
extra={"attachments_count": task.attachments.count()},
|
||
)
|
||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||
|
||
@action(detail=True, methods=["get", "post"], url_path="checklist")
|
||
def checklist(self, request, pk=None):
|
||
task = self.get_object()
|
||
if request.method in ("GET", "HEAD"):
|
||
serializer = TaskChecklistItemSerializer(
|
||
task.checklist_items.select_related("created_by", "completed_by"),
|
||
many=True,
|
||
)
|
||
return Response(serializer.data)
|
||
|
||
serializer = TaskChecklistItemSerializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
position = serializer.validated_data.get("position")
|
||
if position is None:
|
||
max_position = (
|
||
task.checklist_items.aggregate(Max("position"))["position__max"] or 0
|
||
)
|
||
position = max_position + 1000
|
||
is_completed = serializer.validated_data.get("is_completed", False)
|
||
item = serializer.save(
|
||
task=task,
|
||
created_by=request.user,
|
||
position=position,
|
||
completed_by=request.user if is_completed else None,
|
||
completed_at=timezone.now() if is_completed else None,
|
||
)
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.CHECKLIST_ITEM_ADDED,
|
||
object_kind=TaskActivityObjectKind.CHECKLIST_ITEM,
|
||
object_id=item.id,
|
||
metadata={
|
||
"item_title": item.title,
|
||
"is_completed": item.is_completed,
|
||
},
|
||
)
|
||
send_task_checklist_update(task, "checklist_updated", item.id)
|
||
return Response(
|
||
TaskChecklistItemSerializer(item).data,
|
||
status=status.HTTP_201_CREATED,
|
||
)
|
||
|
||
@action(
|
||
detail=True,
|
||
methods=["patch", "delete"],
|
||
url_path=r"checklist/(?P<item_id>\d+)",
|
||
url_name="checklist-item-detail",
|
||
)
|
||
def checklist_item_detail(self, request, pk=None, item_id=None):
|
||
task = self.get_object()
|
||
item = get_object_or_404(task.checklist_items, id=item_id)
|
||
|
||
if request.method == "DELETE":
|
||
item_id = item.id
|
||
item_title = item.title
|
||
item.delete()
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.CHECKLIST_ITEM_REMOVED,
|
||
object_kind=TaskActivityObjectKind.CHECKLIST_ITEM,
|
||
object_id=item_id,
|
||
metadata={"item_title": item_title},
|
||
)
|
||
send_task_checklist_update(task, "checklist_updated", item_id)
|
||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||
|
||
serializer = TaskChecklistItemSerializer(
|
||
item,
|
||
data=request.data,
|
||
partial=True,
|
||
)
|
||
serializer.is_valid(raise_exception=True)
|
||
previous_title = item.title
|
||
previous_completed = item.is_completed
|
||
changed_fields = [
|
||
field
|
||
for field, value in serializer.validated_data.items()
|
||
if getattr(item, field) != value
|
||
]
|
||
if not changed_fields:
|
||
return Response(TaskChecklistItemSerializer(item).data)
|
||
|
||
completion_metadata = {}
|
||
if "is_completed" in serializer.validated_data:
|
||
is_completed = serializer.validated_data["is_completed"]
|
||
completion_metadata = {
|
||
"completed_by": request.user if is_completed else None,
|
||
"completed_at": timezone.now() if is_completed else None,
|
||
}
|
||
item = serializer.save(**completion_metadata)
|
||
if previous_completed != item.is_completed:
|
||
activity_action = (
|
||
TaskActivityAction.CHECKLIST_ITEM_COMPLETED
|
||
if item.is_completed
|
||
else TaskActivityAction.CHECKLIST_ITEM_REOPENED
|
||
)
|
||
else:
|
||
activity_action = TaskActivityAction.CHECKLIST_ITEM_UPDATED
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
activity_action,
|
||
object_kind=TaskActivityObjectKind.CHECKLIST_ITEM,
|
||
object_id=item.id,
|
||
metadata={
|
||
"item_title": item.title,
|
||
"previous_title": previous_title,
|
||
"is_completed": item.is_completed,
|
||
"fields": changed_fields,
|
||
},
|
||
)
|
||
send_task_checklist_update(task, "checklist_updated", item.id)
|
||
return Response(TaskChecklistItemSerializer(item).data)
|
||
|
||
@action(detail=True, methods=["get", "post"], url_path="external-links")
|
||
def external_links(self, request, pk=None):
|
||
task = self.get_object()
|
||
if request.method in ("GET", "HEAD"):
|
||
serializer = TaskExternalLinkSerializer(
|
||
task.external_links.select_related("created_by"),
|
||
many=True,
|
||
)
|
||
return Response(serializer.data)
|
||
|
||
input_serializer = TaskExternalLinkSerializer(data=request.data)
|
||
input_serializer.is_valid(raise_exception=True)
|
||
url = input_serializer.validated_data["url"]
|
||
title = input_serializer.validated_data.get("title", "")
|
||
default_title = title or urlsplit(url).netloc or url
|
||
link, created = TaskExternalLink.objects.get_or_create(
|
||
task=task,
|
||
url=url,
|
||
defaults={
|
||
"title": default_title,
|
||
"created_by": request.user,
|
||
},
|
||
)
|
||
if not created and title and title != link.title:
|
||
link.title = title
|
||
link.save(update_fields=["title"])
|
||
|
||
if created:
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.LINKED,
|
||
object_kind=TaskActivityObjectKind.EXTERNAL_LINK,
|
||
object_id=link.id,
|
||
metadata={
|
||
"object_label": link.title or link.url,
|
||
"object_type": "Ссылка",
|
||
"url": link.url,
|
||
},
|
||
)
|
||
send_task_board_update(
|
||
task.board,
|
||
"linked",
|
||
"external_link",
|
||
link.id,
|
||
extra={"task_id": task.id, "link_id": link.id},
|
||
)
|
||
serializer = TaskExternalLinkSerializer(link)
|
||
return Response(
|
||
serializer.data,
|
||
status=(
|
||
status.HTTP_201_CREATED if created else status.HTTP_200_OK
|
||
),
|
||
)
|
||
|
||
@action(
|
||
detail=True,
|
||
methods=["delete"],
|
||
url_path=r"external-links/(?P<link_id>\d+)",
|
||
url_name="external-link-detail",
|
||
)
|
||
def external_link_detail(self, request, pk=None, link_id=None):
|
||
task = self.get_object()
|
||
link = get_object_or_404(task.external_links, id=link_id)
|
||
link_id = link.id
|
||
title = link.title or link.url
|
||
url = link.url
|
||
link.delete()
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.UNLINKED,
|
||
object_kind=TaskActivityObjectKind.EXTERNAL_LINK,
|
||
object_id=link_id,
|
||
metadata={
|
||
"object_label": title,
|
||
"object_type": "Ссылка",
|
||
"url": url,
|
||
},
|
||
)
|
||
send_task_board_update(
|
||
task.board,
|
||
"unlinked",
|
||
"external_link",
|
||
link_id,
|
||
extra={"task_id": task.id, "link_id": link_id},
|
||
)
|
||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||
|
||
def _comment_payload(self, task, message):
|
||
return {
|
||
"id": message.id,
|
||
"task": task.id,
|
||
"author": EmployeeBriefSerializer(message.author).data,
|
||
"text": message.content,
|
||
"is_edited": message.is_edited,
|
||
"edited_at": message.edited_at,
|
||
"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,
|
||
)
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.COMMENT_ADDED,
|
||
object_kind=TaskActivityObjectKind.COMMENT,
|
||
object_id=message.id,
|
||
metadata={"comment_text": message.content},
|
||
)
|
||
notify_task_comment(task, message)
|
||
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=["patch", "delete"],
|
||
url_path=r"comments/(?P<comment_id>\d+)",
|
||
url_name="delete-comment",
|
||
)
|
||
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 request.method == "PATCH":
|
||
if message.author_id != request.user.id:
|
||
return Response(
|
||
{"detail": "Редактировать комментарий может только его автор."},
|
||
status=status.HTTP_403_FORBIDDEN,
|
||
)
|
||
|
||
text = request.data.get("text", "").strip()
|
||
if not text:
|
||
return Response(
|
||
{"text": ["Это поле не может быть пустым."]},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
previous_text = message.content
|
||
if text == previous_text:
|
||
return Response(self._comment_payload(task, message))
|
||
|
||
comments_helpers.update_comment(
|
||
message,
|
||
text,
|
||
edited_by=request.user,
|
||
)
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.COMMENT_EDITED,
|
||
object_kind=TaskActivityObjectKind.COMMENT,
|
||
object_id=message.id,
|
||
metadata={
|
||
"previous_text": previous_text,
|
||
"comment_text": message.content,
|
||
},
|
||
)
|
||
send_task_board_update(
|
||
task.board,
|
||
"comment_edited",
|
||
"task",
|
||
task.id,
|
||
extra={"comment_id": message.id},
|
||
)
|
||
return Response(self._comment_payload(task, message))
|
||
|
||
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,
|
||
)
|
||
|
||
comment_text = message.content
|
||
comments_helpers.delete_comment(
|
||
message=message,
|
||
deleted_by=request.user,
|
||
soft_delete=True,
|
||
)
|
||
create_task_activity(
|
||
task,
|
||
request.user,
|
||
TaskActivityAction.COMMENT_REMOVED,
|
||
object_kind=TaskActivityObjectKind.COMMENT,
|
||
object_id=message.id,
|
||
metadata={"comment_text": comment_text},
|
||
)
|
||
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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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<link_id>\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)
|