469 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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