469 lines
17 KiB
Python
469 lines
17 KiB
Python
from django.db.models import Max
|
||
from django.contrib.auth import get_user_model
|
||
from drf_spectacular.utils import extend_schema_field
|
||
from rest_framework import serializers
|
||
|
||
from api.v1.employees.serializers import EmployeeBriefSerializer
|
||
from communications.comments_helpers import get_comment_count
|
||
from tasks.access import (
|
||
user_can_access_task_board,
|
||
)
|
||
from tasks.models import (
|
||
Task,
|
||
TaskCover,
|
||
TaskCoverType,
|
||
TaskLabel,
|
||
TaskLinkedObjectKind,
|
||
)
|
||
from .linked import TaskCoverSerializer
|
||
from .structure import TaskLabelSerializer
|
||
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
class TaskSerializer(serializers.ModelSerializer):
|
||
created_by = EmployeeBriefSerializer(read_only=True)
|
||
assignee = EmployeeBriefSerializer(read_only=True)
|
||
participants = EmployeeBriefSerializer(many=True, read_only=True)
|
||
participant_ids = serializers.PrimaryKeyRelatedField(
|
||
source="participants",
|
||
# Existing tasks may still contain dismissed employees. Resolve them here
|
||
# and distinguish retained historical participants from newly added ones
|
||
# in validate(), otherwise any edit that resubmits the participant list
|
||
# becomes impossible after an employee is deactivated.
|
||
queryset=User.objects.all(),
|
||
many=True,
|
||
required=False,
|
||
write_only=True,
|
||
)
|
||
assignee_id = serializers.PrimaryKeyRelatedField(
|
||
source="assignee",
|
||
queryset=User.objects.all(),
|
||
required=False,
|
||
allow_null=True,
|
||
write_only=True,
|
||
)
|
||
labels = TaskLabelSerializer(many=True, read_only=True)
|
||
label_ids = serializers.PrimaryKeyRelatedField(
|
||
source="labels",
|
||
queryset=TaskLabel.objects.all(),
|
||
many=True,
|
||
required=False,
|
||
write_only=True,
|
||
)
|
||
priority_display = serializers.CharField(
|
||
source="get_priority_display",
|
||
read_only=True,
|
||
)
|
||
board_name = serializers.CharField(source="board.name", read_only=True)
|
||
column_name = serializers.CharField(source="column.name", read_only=True)
|
||
column_color = serializers.CharField(source="column.color", read_only=True)
|
||
row_name = serializers.CharField(source="row.name", read_only=True)
|
||
row_color = serializers.CharField(source="row.color", read_only=True)
|
||
linked_posts_count = serializers.SerializerMethodField()
|
||
linked_messages_count = serializers.SerializerMethodField()
|
||
linked_events_count = serializers.SerializerMethodField()
|
||
linked_documents_count = serializers.SerializerMethodField()
|
||
linked_requests_count = serializers.SerializerMethodField()
|
||
linked_procurement_requests_count = serializers.SerializerMethodField()
|
||
linked_employees_count = serializers.SerializerMethodField()
|
||
linked_guests_count = serializers.SerializerMethodField()
|
||
linked_guest_visits_count = serializers.SerializerMethodField()
|
||
linked_attendance_records_count = serializers.SerializerMethodField()
|
||
linked_objects_count = serializers.SerializerMethodField()
|
||
comments_count = serializers.SerializerMethodField()
|
||
attachments_count = serializers.SerializerMethodField()
|
||
cover = serializers.SerializerMethodField()
|
||
cover_attachment = serializers.SerializerMethodField()
|
||
checklist_total = serializers.SerializerMethodField()
|
||
checklist_completed = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Task
|
||
fields = [
|
||
"id",
|
||
"board",
|
||
"board_name",
|
||
"column",
|
||
"column_name",
|
||
"column_color",
|
||
"row",
|
||
"row_name",
|
||
"row_color",
|
||
"title",
|
||
"description",
|
||
"created_by",
|
||
"assignee",
|
||
"assignee_id",
|
||
"participants",
|
||
"participant_ids",
|
||
"labels",
|
||
"label_ids",
|
||
"priority",
|
||
"priority_display",
|
||
"due_date",
|
||
"position",
|
||
"completed_at",
|
||
"linked_posts_count",
|
||
"linked_messages_count",
|
||
"linked_events_count",
|
||
"linked_documents_count",
|
||
"linked_requests_count",
|
||
"linked_procurement_requests_count",
|
||
"linked_employees_count",
|
||
"linked_guests_count",
|
||
"linked_guest_visits_count",
|
||
"linked_attendance_records_count",
|
||
"linked_objects_count",
|
||
"comments_count",
|
||
"attachments_count",
|
||
"cover",
|
||
"cover_attachment",
|
||
"checklist_total",
|
||
"checklist_completed",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
read_only_fields = [
|
||
"id",
|
||
"created_by",
|
||
"participants",
|
||
"board_name",
|
||
"column_name",
|
||
"column_color",
|
||
"row_name",
|
||
"row_color",
|
||
"priority_display",
|
||
"completed_at",
|
||
"linked_posts_count",
|
||
"linked_messages_count",
|
||
"linked_events_count",
|
||
"linked_documents_count",
|
||
"linked_requests_count",
|
||
"linked_procurement_requests_count",
|
||
"linked_employees_count",
|
||
"linked_guests_count",
|
||
"linked_guest_visits_count",
|
||
"linked_attendance_records_count",
|
||
"linked_objects_count",
|
||
"comments_count",
|
||
"attachments_count",
|
||
"cover",
|
||
"cover_attachment",
|
||
"checklist_total",
|
||
"checklist_completed",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_posts_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_posts_count",
|
||
TaskLinkedObjectKind.POST,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_messages_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_messages_count",
|
||
TaskLinkedObjectKind.MESSAGE,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_events_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_events_count",
|
||
TaskLinkedObjectKind.CALENDAR_EVENT,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_documents_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_documents_count",
|
||
TaskLinkedObjectKind.DOCUMENT,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_requests_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_requests_count",
|
||
TaskLinkedObjectKind.REQUEST,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_procurement_requests_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_procurement_requests_count",
|
||
TaskLinkedObjectKind.PROCUREMENT_REQUEST,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_employees_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_employees_count",
|
||
TaskLinkedObjectKind.EMPLOYEE,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_guests_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_guests_count",
|
||
TaskLinkedObjectKind.GUEST,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_guest_visits_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_guest_visits_count",
|
||
TaskLinkedObjectKind.GUEST_VISIT,
|
||
)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_attendance_records_count(self, obj):
|
||
return self._get_linked_kind_count(
|
||
obj,
|
||
"linked_attendance_records_count",
|
||
TaskLinkedObjectKind.ATTENDANCE_RECORD,
|
||
)
|
||
|
||
def _get_linked_kind_count(self, obj, attribute_name, kind):
|
||
if hasattr(obj, attribute_name):
|
||
return getattr(obj, attribute_name)
|
||
prefetched = getattr(obj, "_prefetched_objects_cache", {}).get("linked_objects")
|
||
if prefetched is not None:
|
||
return sum(link.kind == kind for link in prefetched)
|
||
return obj.linked_objects.filter(kind=kind).count()
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_linked_objects_count(self, obj):
|
||
if hasattr(obj, "linked_objects_count"):
|
||
return obj.linked_objects_count
|
||
return obj.linked_objects.count() + obj.external_links.count()
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_comments_count(self, obj):
|
||
if hasattr(obj, "comments_count"):
|
||
return obj.comments_count
|
||
return get_comment_count(obj)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_attachments_count(self, obj):
|
||
if hasattr(obj, "attachments_count"):
|
||
return obj.attachments_count
|
||
return obj.attachments.count()
|
||
|
||
def _get_cover_payload(self, obj):
|
||
if hasattr(obj, "_task_cover_payload"):
|
||
return obj._task_cover_payload
|
||
try:
|
||
cover = obj.cover
|
||
except TaskCover.DoesNotExist:
|
||
cover = None
|
||
if (
|
||
cover is not None
|
||
and cover.kind == TaskCoverType.CHECKLIST
|
||
and not obj.checklist_items.exists()
|
||
):
|
||
cover = None
|
||
payload = (
|
||
TaskCoverSerializer(cover, context=self.context).data
|
||
if cover is not None
|
||
else None
|
||
)
|
||
obj._task_cover_payload = payload
|
||
return payload
|
||
|
||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||
def get_cover(self, obj):
|
||
return self._get_cover_payload(obj)
|
||
|
||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||
def get_cover_attachment(self, obj):
|
||
payload = self._get_cover_payload(obj)
|
||
return payload.get("attachment") if payload else None
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_checklist_total(self, obj):
|
||
if hasattr(obj, "checklist_total"):
|
||
return obj.checklist_total
|
||
return obj.checklist_items.count()
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_checklist_completed(self, obj):
|
||
if hasattr(obj, "checklist_completed"):
|
||
return obj.checklist_completed
|
||
prefetched = getattr(obj, "_prefetched_objects_cache", {}).get(
|
||
"checklist_items"
|
||
)
|
||
if prefetched is not None:
|
||
return sum(item.is_completed for item in prefetched)
|
||
return obj.checklist_items.filter(is_completed=True).count()
|
||
|
||
def validate(self, attrs):
|
||
request = self.context.get("request")
|
||
user = getattr(request, "user", None)
|
||
board = attrs.get("board") or getattr(self.instance, "board", None)
|
||
column = attrs.get("column") or getattr(self.instance, "column", None)
|
||
row = (
|
||
attrs.get("row") if "row" in attrs else getattr(self.instance, "row", None)
|
||
)
|
||
if board and not user_can_access_task_board(user, board):
|
||
raise serializers.ValidationError(
|
||
{"board": "У вас нет доступа к этой доске."}
|
||
)
|
||
|
||
if column and board and column.board_id != board.id:
|
||
raise serializers.ValidationError(
|
||
{"column": "Колонка должна принадлежать выбранной доске."}
|
||
)
|
||
|
||
if column and column.subcolumns.filter(is_archived=False).exists():
|
||
raise serializers.ValidationError(
|
||
{"column": "Выберите подколонку для размещения задачи."}
|
||
)
|
||
|
||
if row and column:
|
||
top_level_column_id = column.parent_id or column.id
|
||
if row.column_id != top_level_column_id:
|
||
raise serializers.ValidationError(
|
||
{"row": "Дорожка должна принадлежать основной колонке задачи."}
|
||
)
|
||
|
||
self._validate_task_users(attrs, board)
|
||
return attrs
|
||
|
||
def _validate_task_users(self, attrs, board):
|
||
existing_assignee_id = (
|
||
self.instance.assignee_id if self.instance is not None else None
|
||
)
|
||
board_changed = bool(
|
||
board
|
||
and (
|
||
self.instance is None
|
||
or self.instance.board_id != board.id
|
||
)
|
||
)
|
||
assignee = attrs.get("assignee")
|
||
if "assignee" in attrs and assignee is not None:
|
||
assignee_changed = assignee.id != existing_assignee_id
|
||
if assignee_changed and not assignee.is_active:
|
||
raise serializers.ValidationError(
|
||
{"assignee_id": "Исполнителем может быть только активный сотрудник."}
|
||
)
|
||
should_check_assignee_access = assignee_changed or (
|
||
board_changed and assignee.is_active
|
||
)
|
||
if should_check_assignee_access and not user_can_access_task_board(
|
||
assignee, board
|
||
):
|
||
raise serializers.ValidationError(
|
||
{
|
||
"assignee_id": (
|
||
"Исполнителем может быть только сотрудник с доступом "
|
||
"к этой доске."
|
||
)
|
||
}
|
||
)
|
||
|
||
participants = attrs.get("participants")
|
||
if participants is not None:
|
||
existing_participant_ids = (
|
||
set(self.instance.participants.values_list("id", flat=True))
|
||
if self.instance is not None
|
||
else set()
|
||
)
|
||
if self.instance is not None:
|
||
existing_participant_ids.add(self.instance.created_by_id)
|
||
if existing_assignee_id:
|
||
existing_participant_ids.add(existing_assignee_id)
|
||
new_participants = [
|
||
participant
|
||
for participant in participants
|
||
if participant.id not in existing_participant_ids
|
||
]
|
||
if any(not participant.is_active for participant in new_participants):
|
||
raise serializers.ValidationError(
|
||
{
|
||
"participant_ids": (
|
||
"Добавлять в задачу можно только активных сотрудников."
|
||
)
|
||
}
|
||
)
|
||
participants_to_check = (
|
||
[participant for participant in participants if participant.is_active]
|
||
if board_changed
|
||
else new_participants
|
||
)
|
||
if board and any(
|
||
not user_can_access_task_board(participant, board)
|
||
for participant in participants_to_check
|
||
):
|
||
raise serializers.ValidationError(
|
||
{
|
||
"participant_ids": (
|
||
"Участниками могут быть только сотрудники с доступом "
|
||
"к этой доске."
|
||
)
|
||
}
|
||
)
|
||
|
||
def create(self, validated_data):
|
||
labels = validated_data.pop("labels", [])
|
||
participants = list(validated_data.pop("participants", []))
|
||
if "position" not in validated_data:
|
||
column = validated_data["column"]
|
||
row = validated_data.get("row")
|
||
max_position = (
|
||
Task.objects.filter(column=column, row=row).aggregate(Max("position"))[
|
||
"position__max"
|
||
]
|
||
or 0
|
||
)
|
||
validated_data["position"] = max_position + 1000
|
||
task = Task.objects.create(**validated_data)
|
||
participant_ids = {participant.id for participant in participants}
|
||
participant_ids.add(task.created_by_id)
|
||
if task.assignee_id:
|
||
participant_ids.add(task.assignee_id)
|
||
task.participants.set(participant_ids)
|
||
if labels:
|
||
task.labels.set(labels)
|
||
return task
|
||
|
||
def update(self, instance, validated_data):
|
||
previous_assignee_id = instance.assignee_id
|
||
labels = validated_data.pop("labels", None)
|
||
participants = validated_data.pop("participants", None)
|
||
task = super().update(instance, validated_data)
|
||
if participants is not None:
|
||
participant_ids = {participant.id for participant in participants}
|
||
participant_ids.add(task.created_by_id)
|
||
if previous_assignee_id:
|
||
participant_ids.add(previous_assignee_id)
|
||
if task.assignee_id:
|
||
participant_ids.add(task.assignee_id)
|
||
task.participants.set(participant_ids)
|
||
else:
|
||
retained_participant_ids = [task.assignee_id, previous_assignee_id]
|
||
task.participants.add(
|
||
*[
|
||
participant_id
|
||
for participant_id in retained_participant_ids
|
||
if participant_id
|
||
]
|
||
)
|
||
if labels is not None:
|
||
task.labels.set(labels)
|
||
return task
|