Corp/backend/api/v1/tasks/serializers.py

2021 lines
70 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 urllib.parse import urlsplit
from django.db.models import Count, Max
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils import timezone
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from attendance.models import AttendanceRecord
from api.v1.documents.serializers import DocumentReadSerializer
from api.v1.employees.serializers import EmployeeBriefSerializer
from api.v1.employees.serializers.department import DepartmentBriefSerializer
from communications.models import Message
from communications.comments_helpers import get_comment_count
from communications.serialization import serialize_message
from communications.utils import user_can_access_chat
from documents.models import Document
from employees.models import Department, EmployeeDepartment
from employees.services.personnel_state import resolve_employee_personnel_state
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 schedule.models import Event
from api.v1.schedule.serializers import EventSerializer
from tasks.access import (
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,
user_can_access_task_board,
)
from tasks.automation.schema import (
AutomationSchemaError,
validate_automation_payload,
)
from tasks.models import (
Task,
TaskActivity,
TaskAttachment,
TaskAutomation,
TaskAutomationKind,
TaskAutomationRun,
TaskBoard,
TaskBoardAccessScope,
TaskBoardGroup,
TaskBoardPlacement,
TaskChecklistItem,
TaskColumn,
TaskColumnRow,
TaskCover,
TaskCoverType,
TaskExternalLink,
TaskLabel,
TaskLinkedObject,
TaskLinkedObjectKind,
TaskUserSettings,
)
User = get_user_model()
DEFAULT_COLUMNS = [
("Новые", "#38bdf8", False),
("В работе", "#f59e0b", False),
("На проверке", "#8b5cf6", False),
("Готово", "#22c55e", True),
]
def create_default_columns(board):
if board.columns.exists():
return
TaskColumn.objects.bulk_create(
[
TaskColumn(
board=board,
name=name,
color=color,
is_done=is_done,
position=index * 1000,
)
for index, (name, color, is_done) in enumerate(DEFAULT_COLUMNS)
]
)
def validate_board_access(serializer, board):
request = serializer.context.get("request")
user = getattr(request, "user", None)
if not user_can_access_task_board(user, board):
raise serializers.ValidationError("У вас нет доступа к этой доске.")
return board
class TaskLabelSerializer(serializers.ModelSerializer):
class Meta:
model = TaskLabel
fields = ["id", "name", "color"]
read_only_fields = ["id"]
def validate_name(self, value):
name = value.strip()
duplicate = TaskLabel.objects.only("id", "name")
if self.instance:
duplicate = duplicate.exclude(pk=self.instance.pk)
normalized_name = name.casefold()
if any(label.name.casefold() == normalized_name for label in duplicate):
raise serializers.ValidationError("Метка с таким названием уже существует.")
return name
class TaskAutomationRunSerializer(serializers.ModelSerializer):
automation_name = serializers.CharField(source="automation.name", read_only=True)
task_title = serializers.CharField(source="task.title", read_only=True)
actor = EmployeeBriefSerializer(read_only=True)
class Meta:
model = TaskAutomationRun
fields = [
"id",
"automation",
"automation_name",
"task",
"task_title",
"trigger_activity",
"actor",
"source",
"status",
"context",
"actions_log",
"error",
"started_at",
"finished_at",
]
read_only_fields = fields
class TaskAutomationSerializer(serializers.ModelSerializer):
"""Validate shared automation rules exposed to authenticated task users.
``can_manage`` intentionally mirrors the temporary broad API permission. It
should use the dedicated automation permission once that role is introduced.
"""
created_by = EmployeeBriefSerializer(read_only=True)
boards = serializers.PrimaryKeyRelatedField(
queryset=TaskBoard.objects.filter(is_archived=False),
many=True,
required=False,
)
runs_count = serializers.IntegerField(read_only=True, default=0)
can_manage = serializers.SerializerMethodField()
class Meta:
model = TaskAutomation
fields = [
"id",
"name",
"description",
"kind",
"trigger",
"conditions",
"actions",
"schedule_config",
"applies_to_all_boards",
"boards",
"is_active",
"stop_on_error",
"position",
"created_by",
"last_run_at",
"runs_count",
"can_manage",
"created_at",
"updated_at",
]
read_only_fields = [
"id",
"created_by",
"last_run_at",
"runs_count",
"can_manage",
"created_at",
"updated_at",
]
def get_can_manage(self, obj):
user = getattr(self.context.get("request"), "user", None)
return bool(user and user.is_authenticated)
def validate_name(self, value):
name = value.strip()
if not name:
raise serializers.ValidationError("Введите название автоматизации.")
return name
def validate(self, attrs):
attrs = super().validate(attrs)
instance = self.instance
kind = attrs.get("kind", getattr(instance, "kind", TaskAutomationKind.EVENT))
trigger = attrs.get("trigger", getattr(instance, "trigger", "task_updated"))
conditions = attrs.get("conditions", getattr(instance, "conditions", {}))
actions = attrs.get("actions", getattr(instance, "actions", []))
schedule_config = attrs.get(
"schedule_config",
getattr(instance, "schedule_config", {}),
)
try:
normalized = validate_automation_payload(
kind=kind,
trigger=trigger,
conditions=conditions,
actions=actions,
schedule_config=schedule_config,
)
except AutomationSchemaError as error:
raise serializers.ValidationError({"detail": str(error)}) from error
attrs.update(normalized)
applies_to_all = attrs.get(
"applies_to_all_boards",
getattr(instance, "applies_to_all_boards", True),
)
boards = attrs.get("boards")
if not applies_to_all and boards is not None and not boards:
raise serializers.ValidationError(
{"boards": "Выберите хотя бы одну доску или включите все доски."}
)
if applies_to_all:
attrs["boards"] = []
return attrs
class TaskColumnSerializer(serializers.ModelSerializer):
tasks_count = serializers.IntegerField(read_only=True, default=0)
subcolumns_count = serializers.SerializerMethodField()
class Meta:
model = TaskColumn
validators = []
fields = [
"id",
"board",
"parent",
"name",
"position",
"color",
"is_done",
"is_archived",
"tasks_count",
"subcolumns_count",
"created_at",
"updated_at",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"tasks_count",
"subcolumns_count",
]
def validate_board(self, board):
return validate_board_access(self, board)
@extend_schema_field(serializers.IntegerField())
def get_subcolumns_count(self, obj):
annotated_count = getattr(obj, "subcolumns_count", None)
if annotated_count is not None:
return annotated_count
return obj.subcolumns.filter(is_archived=False).count()
def validate(self, attrs):
board = attrs.get("board") or getattr(self.instance, "board", None)
parent = attrs.get("parent", getattr(self.instance, "parent", None))
if self.instance and board and self.instance.board_id != board.id:
raise serializers.ValidationError(
{"board": "Доску существующей колонки изменить нельзя."}
)
if self.instance and "parent" in attrs:
current_parent_id = self.instance.parent_id
requested_parent_id = parent.id if parent else None
if current_parent_id != requested_parent_id:
raise serializers.ValidationError(
{"parent": "Родителя существующей колонки изменить нельзя."}
)
if parent:
if board and parent.board_id != board.id:
raise serializers.ValidationError(
{"parent": "Подколонка должна принадлежать той же доске."}
)
if parent.parent_id:
raise serializers.ValidationError(
{"parent": "Допускается только один уровень подколонок."}
)
if parent.is_archived:
raise serializers.ValidationError(
{"parent": "Нельзя добавить подколонку в архивную колонку."}
)
attrs["is_done"] = parent.is_done
if (
self.instance
and attrs.get("is_archived") is True
and self.instance.subcolumns.exists()
):
raise serializers.ValidationError(
{"is_archived": "Сначала удалите подколонки."}
)
name = attrs.get("name", getattr(self.instance, "name", ""))
if board and name:
duplicate_columns = TaskColumn.objects.filter(name=name)
if parent:
duplicate_columns = duplicate_columns.filter(parent=parent)
else:
duplicate_columns = duplicate_columns.filter(
board=board,
parent__isnull=True,
)
if self.instance:
duplicate_columns = duplicate_columns.exclude(pk=self.instance.pk)
if duplicate_columns.exists():
raise serializers.ValidationError(
{"name": "Колонка с таким названием уже существует здесь."}
)
return attrs
def create(self, validated_data):
parent = validated_data.get("parent")
if parent:
validated_data.setdefault("color", parent.color)
validated_data.setdefault("is_done", parent.is_done)
return super().create(validated_data)
class TaskColumnRowSerializer(serializers.ModelSerializer):
tasks_count = serializers.IntegerField(read_only=True, default=0)
class Meta:
model = TaskColumnRow
fields = [
"id",
"column",
"name",
"position",
"color",
"tasks_count",
"created_at",
"updated_at",
]
read_only_fields = ["id", "tasks_count", "created_at", "updated_at"]
def validate_column(self, column):
validate_board_access(self, column.board)
if column.parent_id:
raise serializers.ValidationError(
"Дорожки можно создавать только в основной колонке."
)
if column.is_archived:
raise serializers.ValidationError(
"Нельзя создать дорожку в архивной колонке."
)
return column
def validate(self, attrs):
column = attrs.get("column") or getattr(self.instance, "column", None)
if self.instance and column and self.instance.column_id != column.id:
raise serializers.ValidationError(
{"column": "Основную колонку дорожки изменить нельзя."}
)
name = attrs.get("name", getattr(self.instance, "name", "")).strip()
if not name:
raise serializers.ValidationError({"name": "Укажите название дорожки."})
duplicate = TaskColumnRow.objects.filter(column=column, name__iexact=name)
if self.instance:
duplicate = duplicate.exclude(pk=self.instance.pk)
if duplicate.exists():
raise serializers.ValidationError(
{"name": "Дорожка с таким названием уже существует в колонке."}
)
attrs["name"] = name
return attrs
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",
queryset=User.objects.filter(is_active=True),
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)
)
participants = attrs.get("participants")
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": "Дорожка должна принадлежать основной колонке задачи."}
)
if participants is not None and board:
inaccessible_participants = [
participant.id
for participant in participants
if not user_can_access_task_board(participant, board)
]
if inaccessible_participants:
raise serializers.ValidationError(
{
"participant_ids": (
"Участниками могут быть только сотрудники с доступом "
"к этой доске."
)
}
)
return attrs
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
class TaskBoardSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
members = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
many=True,
required=False,
)
member_details = EmployeeBriefSerializer(
source="members",
many=True,
read_only=True,
)
departments = serializers.PrimaryKeyRelatedField(
queryset=Department.objects.all(),
many=True,
required=False,
)
department_details = DepartmentBriefSerializer(
source="departments",
many=True,
read_only=True,
)
columns = TaskColumnSerializer(many=True, read_only=True)
column_rows = serializers.SerializerMethodField()
tasks = TaskSerializer(many=True, read_only=True)
labels = serializers.SerializerMethodField()
tasks_count = serializers.IntegerField(read_only=True, default=0)
is_default_for_current_user = serializers.SerializerMethodField()
group_id = serializers.IntegerField(
source="current_user_group_id",
read_only=True,
allow_null=True,
default=None,
)
is_pinned = serializers.BooleanField(
source="current_user_is_pinned",
read_only=True,
default=False,
)
notifications_enabled = serializers.BooleanField(
source="current_user_notifications_enabled",
read_only=True,
default=True,
)
view_state = serializers.SerializerMethodField()
class Meta:
model = TaskBoard
fields = [
"id",
"name",
"description",
"avatar",
"access_scope",
"created_by",
"members",
"member_details",
"departments",
"department_details",
"is_archived",
"columns",
"column_rows",
"tasks",
"labels",
"tasks_count",
"is_default_for_current_user",
"group_id",
"is_pinned",
"notifications_enabled",
"view_state",
"created_at",
"updated_at",
]
@extend_schema_field(TaskLabelSerializer(many=True))
def get_labels(self, obj):
labels = getattr(self, "_global_labels_cache", None)
if labels is None:
labels = list(TaskLabel.objects.order_by("name", "id"))
self._global_labels_cache = labels
return TaskLabelSerializer(labels, many=True).data
def validate(self, attrs):
attrs = super().validate(attrs)
access_scope = attrs.get("access_scope")
if access_scope is None:
has_members = bool(attrs.get("members"))
has_departments = bool(attrs.get("departments"))
if has_members or has_departments:
access_scope = TaskBoardAccessScope.RESTRICTED
elif "members" in attrs and "departments" in attrs:
access_scope = TaskBoardAccessScope.ALL
else:
access_scope = getattr(
self.instance,
"access_scope",
TaskBoardAccessScope.ALL,
)
attrs["access_scope"] = access_scope
if access_scope != TaskBoardAccessScope.RESTRICTED:
attrs["members"] = []
attrs["departments"] = []
return attrs
@extend_schema_field(serializers.DictField())
def get_view_state(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return {
"hidden_column_ids": [],
"collapsed_base_column_ids": [],
"collapsed_row_ids": [],
}
raw_state = (
TaskBoardPlacement.objects.filter(user=user, board=obj)
.values_list("view_state", flat=True)
.first()
or {}
)
valid_column_ids = set(
obj.columns.filter(parent__isnull=True, is_archived=False).values_list(
"id", flat=True
)
)
valid_row_ids = set(
TaskColumnRow.objects.filter(
column__board=obj,
column__is_archived=False,
).values_list("id", flat=True)
)
def valid_ids(key, allowed_ids):
values = raw_state.get(key, []) if isinstance(raw_state, dict) else []
if not isinstance(values, list):
return []
return sorted(
{
value
for value in values
if isinstance(value, int) and value in allowed_ids
}
)
return {
"hidden_column_ids": valid_ids("hidden_column_ids", valid_column_ids),
"collapsed_base_column_ids": valid_ids(
"collapsed_base_column_ids", valid_column_ids
),
"collapsed_row_ids": valid_ids("collapsed_row_ids", valid_row_ids),
}
@extend_schema_field(TaskColumnRowSerializer(many=True))
def get_column_rows(self, obj):
rows = (
TaskColumnRow.objects.filter(
column__board=obj,
)
.annotate(tasks_count=Count("tasks", distinct=True))
.order_by("column_id", "position", "id")
)
return TaskColumnRowSerializer(rows, many=True).data
@extend_schema_field(serializers.BooleanField())
def get_is_default_for_current_user(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return False
settings_obj = getattr(user, "task_settings", None)
return bool(settings_obj and settings_obj.default_board_id == obj.id)
class TaskBoardSummarySerializer(TaskBoardSerializer):
open_tasks_count = serializers.IntegerField(read_only=True, default=0)
completed_tasks_count = serializers.IntegerField(read_only=True, default=0)
class Meta(TaskBoardSerializer.Meta):
fields = [
"id",
"name",
"description",
"avatar",
"access_scope",
"created_by",
"member_details",
"department_details",
"tasks_count",
"open_tasks_count",
"completed_tasks_count",
"is_default_for_current_user",
"group_id",
"is_pinned",
"notifications_enabled",
"created_at",
"updated_at",
]
class TaskBoardGroupSerializer(serializers.ModelSerializer):
boards_count = serializers.IntegerField(read_only=True, default=0)
is_default_for_current_user = serializers.SerializerMethodField()
class Meta:
model = TaskBoardGroup
fields = [
"id",
"name",
"position",
"boards_count",
"is_default_for_current_user",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def validate_name(self, value):
name = value.strip()
if not name:
raise serializers.ValidationError("Укажите название группы.")
request = self.context.get("request")
queryset = TaskBoardGroup.objects.filter(
user=getattr(request, "user", None),
name__iexact=name,
)
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
if queryset.exists():
raise serializers.ValidationError(
"Группа с таким названием уже существует."
)
return name
@extend_schema_field(serializers.BooleanField())
def get_is_default_for_current_user(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return False
if not hasattr(self, "_default_group_id"):
self._default_group_id = (
TaskUserSettings.objects.filter(user=user)
.values_list("default_group_id", flat=True)
.first()
)
return self._default_group_id == obj.id
class TaskLinkedPostSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
post = serializers.SerializerMethodField()
post_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"post_id",
"post",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_post(self, obj):
if obj.kind != TaskLinkedObjectKind.POST:
return None
post = getattr(obj, "content_object", None)
if post is None:
return None
request = self.context.get("request")
from api.v1.utils import build_media_url
return {
"id": post.id,
"type": post.type,
"department": post.department_id,
"department_id": post.department_id,
"department_name": getattr(post.department, "name", None),
"title": post.title,
"body": post.body,
"image": build_media_url(post.image, request),
"attachment": build_media_url(post.attachment, request),
"created_at": post.created_at,
"pinned": post.pinned_global or post.pinned_department,
"pinned_global": post.pinned_global,
"pinned_department": post.pinned_department,
"likes_count": post.likes_count,
"comments_count": get_comment_count(post),
"author_id": post.author_id,
"author": EmployeeBriefSerializer(post.author).data,
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
post = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.POST or post is None or not user:
return False
return user_can_access_feed_post(user, post)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
post = getattr(obj, "content_object", None)
if post is None or not self.get_can_open(obj):
return None
return f"/?post={post.id}"
def get_post_content_type():
return ContentType.objects.get_for_model(Post)
class TaskLinkedMessageSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
message = serializers.SerializerMethodField()
message_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"message_id",
"message",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_message(self, obj):
if obj.kind != TaskLinkedObjectKind.MESSAGE:
return None
message = getattr(obj, "content_object", None)
if message is None:
return None
return serialize_message(message, include_linked_tasks=False)
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
message = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.MESSAGE or message is None or not user:
return False
return user_can_access_chat(message.chat, user)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
message = getattr(obj, "content_object", None)
if message is None or not self.get_can_open(obj):
return None
return f"/messages/{message.chat_id}?message={message.id}"
def get_message_content_type():
return ContentType.objects.get_for_model(Message)
class TaskActivitySerializer(serializers.ModelSerializer):
actor = EmployeeBriefSerializer(read_only=True)
action_display = serializers.CharField(source="get_action_display", read_only=True)
class Meta:
model = TaskActivity
fields = [
"id",
"action",
"action_display",
"actor",
"object_kind",
"object_id",
"metadata",
"created_at",
]
read_only_fields = fields
class TaskAttachmentSerializer(serializers.ModelSerializer):
uploaded_by = EmployeeBriefSerializer(read_only=True)
download_url = serializers.SerializerMethodField()
thumbnail_url = serializers.SerializerMethodField()
class Meta:
model = TaskAttachment
fields = [
"id",
"task",
"comment",
"file_name",
"file_size",
"mime_type",
"uploaded_by",
"created_at",
"download_url",
"thumbnail_url",
]
read_only_fields = fields
@extend_schema_field(serializers.URLField())
def get_download_url(self, obj):
url = reverse(
"api:v1:tasks:task-attachment-download",
kwargs={"pk": obj.task_id, "attachment_id": obj.id},
)
request = self.context.get("request")
return request.build_absolute_uri(url) if request else url
@extend_schema_field(serializers.URLField(allow_null=True))
def get_thumbnail_url(self, obj):
from tasks.media import task_attachment_media_kind
if not task_attachment_media_kind(obj):
return None
url = reverse(
"api:v1:tasks:task-attachment-thumbnail",
kwargs={"pk": obj.task_id, "attachment_id": obj.id},
)
request = self.context.get("request")
return request.build_absolute_uri(url) if request else url
class TaskChecklistItemSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
completed_by = EmployeeBriefSerializer(read_only=True)
class Meta:
model = TaskChecklistItem
fields = [
"id",
"task",
"title",
"position",
"is_completed",
"created_by",
"completed_by",
"completed_at",
"created_at",
"updated_at",
]
read_only_fields = [
"id",
"task",
"created_by",
"completed_by",
"completed_at",
"created_at",
"updated_at",
]
def validate_title(self, value):
value = value.strip()
if not value:
raise serializers.ValidationError("Введите текст пункта чек-листа.")
return value
class TaskExternalLinkSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
class Meta:
model = TaskExternalLink
fields = ["id", "task", "url", "title", "created_by", "created_at"]
read_only_fields = ["id", "task", "created_by", "created_at"]
def validate_url(self, value):
value = value.strip()
if urlsplit(value).scheme.lower() not in {"http", "https"}:
raise serializers.ValidationError(
"Разрешены только ссылки с протоколом http или https."
)
return value
def validate_title(self, value):
return value.strip()
class TaskLinkedCalendarEventSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
event = serializers.SerializerMethodField()
event_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"event_id",
"event",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_event(self, obj):
if obj.kind != TaskLinkedObjectKind.CALENDAR_EVENT:
return None
event = getattr(obj, "content_object", None)
if event is None:
return None
return EventSerializer(event, context=self.context).data
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
event = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.CALENDAR_EVENT or event is None or not user:
return False
return user_can_access_calendar_event(user, event)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
event = getattr(obj, "content_object", None)
if event is None or not self.get_can_open(obj):
return None
return f"/calendar?calendar={event.calendar_id}&event={event.id}"
def get_calendar_event_content_type():
return ContentType.objects.get_for_model(Event)
class TaskLinkedDocumentSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
document = serializers.SerializerMethodField()
document_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"document_id",
"document",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_document(self, obj):
if obj.kind != TaskLinkedObjectKind.DOCUMENT:
return None
document = getattr(obj, "content_object", None)
if document is None:
return None
if not self.get_can_open(obj):
return {
"id": document.id,
"title": document.title,
"description": document.description,
"is_regulation": document.is_regulation,
"folder_path": document.folder_path,
"file_name": (
getattr(document.file, "name", None) if document.file else None
),
}
return DocumentReadSerializer(document, context=self.context).data
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
document = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.DOCUMENT or document is None or not user:
return False
return user_can_access_document(user, document)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
document = getattr(obj, "content_object", None)
if document is None or not self.get_can_open(obj):
return None
return f"/documents?document={document.id}"
def get_document_content_type():
return ContentType.objects.get_for_model(Document)
class TaskLinkedRequestSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
request = serializers.SerializerMethodField()
request_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"request_id",
"request",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_request(self, obj):
if obj.kind != TaskLinkedObjectKind.REQUEST:
return None
request_obj = getattr(obj, "content_object", None)
if request_obj is None:
return None
return {
"id": request_obj.id,
"title": request_obj.title,
"display_title": request_obj.display_title,
"type": request_obj.type,
"type_display": request_obj.get_type_display(),
"status": request_obj.status,
"status_display": request_obj.get_status_display(),
"comment": request_obj.comment,
"date_from": request_obj.date_from,
"date_to": request_obj.date_to,
"employee": EmployeeBriefSerializer(request_obj.employee).data,
"created_at": request_obj.created_at,
"updated_at": request_obj.updated_at,
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
request_obj = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.REQUEST or request_obj is None or not user:
return False
return user_can_access_employee_request(user, request_obj)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
request_obj = getattr(obj, "content_object", None)
if request_obj is None or not self.get_can_open(obj):
return None
return f"/requests?request={request_obj.id}"
def get_request_content_type():
return ContentType.objects.get_for_model(EmployeeRequest)
class TaskLinkedProcurementRequestSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
procurement_request = serializers.SerializerMethodField()
procurement_request_id = serializers.IntegerField(
source="object_id", read_only=True
)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"procurement_request_id",
"procurement_request",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_procurement_request(self, obj):
if obj.kind != TaskLinkedObjectKind.PROCUREMENT_REQUEST:
return None
procurement_request = getattr(obj, "content_object", None)
if procurement_request is None:
return None
return {
"id": procurement_request.id,
"title": procurement_request.title,
"description": procurement_request.description,
"status": procurement_request.status,
"status_display": procurement_request.get_status_display(),
"urgency": procurement_request.urgency,
"urgency_display": procurement_request.get_urgency_display(),
"fulfillment_status": procurement_request.fulfillment_status,
"fulfillment_status_display": procurement_request.get_fulfillment_status_display(),
"department_id": procurement_request.department_id,
"department_name": (
procurement_request.department.name
if procurement_request.department_id
else None
),
"processing_department_id": procurement_request.processing_department_id,
"processing_department_name": (
procurement_request.processing_department.name
if procurement_request.processing_department_id
else None
),
"requestor": EmployeeBriefSerializer(procurement_request.requestor).data,
"executor": (
EmployeeBriefSerializer(procurement_request.executor).data
if procurement_request.executor_id
else None
),
"total_cost": procurement_request.total_cost,
"items_count": procurement_request.items_count,
"created_at": procurement_request.created_at,
"updated_at": procurement_request.updated_at,
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
procurement_request = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if (
obj.kind != TaskLinkedObjectKind.PROCUREMENT_REQUEST
or procurement_request is None
or not user
):
return False
return user_can_access_procurement_request(user, procurement_request)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
procurement_request = getattr(obj, "content_object", None)
if procurement_request is None or not self.get_can_open(obj):
return None
return f"/procurement?request={procurement_request.id}"
def get_procurement_request_content_type():
return ContentType.objects.get_for_model(ProcurementRequest)
class TaskLinkedEmployeeSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
employee = serializers.SerializerMethodField()
employee_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"employee_id",
"employee",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_employee(self, obj):
if obj.kind != TaskLinkedObjectKind.EMPLOYEE:
return None
employee = getattr(obj, "content_object", None)
if employee is None:
return None
departments = []
links = getattr(employee, "dept_links", None)
if links is None:
links = EmployeeDepartment.objects.select_related(
"department",
"role",
).filter(employee=employee, is_active=True)
departments = [
{
"id": link.department_id,
"name": link.department.name if link.department_id else None,
"role_id": link.role_id,
"role_name": link.role.name if link.role_id else None,
"is_head": (
link.department.head_id == employee.id
if link.department_id
else False
),
}
for link in links
]
personnel_state = resolve_employee_personnel_state(
employee,
timezone.localdate(),
)
return {
**EmployeeBriefSerializer(employee).data,
"position": (
{
"id": employee.position_id,
"name": employee.position.name,
}
if employee.position_id
else None
),
"departments": departments,
"is_active": employee.is_active,
"personnel_state": {
"status": personnel_state.status,
"label": personnel_state.label or "Работает",
"action_id": personnel_state.action_id,
"date_from": personnel_state.date_from,
"date_to": personnel_state.date_to,
"expects_attendance": personnel_state.expects_attendance,
},
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
employee = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.EMPLOYEE or employee is None or not user:
return False
return user_can_access_employee(user, employee)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
employee = getattr(obj, "content_object", None)
if employee is None or not self.get_can_open(obj):
return None
return f"/users/{employee.id}"
def get_employee_content_type():
return ContentType.objects.get_for_model(User)
def _guest_payload(guest):
return {
"id": guest.id,
"full_name": guest.full_name,
"last_name": guest.last_name,
"first_name": guest.first_name,
"patronymic": guest.patronymic,
"birth_date": guest.birth_date,
"phone": guest.phone,
"email": guest.email,
"avatar": guest.avatar.url if getattr(guest, "avatar", None) else None,
"organization": guest.organization,
"position": guest.position,
"is_active": guest.is_active,
"is_blacklisted": guest.is_blacklisted,
"ldap_username": guest.ldap_username,
"ldap_upn": guest.ldap_upn,
"created_at": guest.created_at,
"updated_at": guest.updated_at,
}
class TaskLinkedGuestSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
guest = serializers.SerializerMethodField()
guest_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"guest_id",
"guest",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_guest(self, obj):
if obj.kind != TaskLinkedObjectKind.GUEST:
return None
guest = getattr(obj, "content_object", None)
if guest is None:
return None
return _guest_payload(guest)
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
guest = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.GUEST or guest is None or not user:
return False
return user_can_access_guest(user, guest)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
guest = getattr(obj, "content_object", None)
if guest is None or not self.get_can_open(obj):
return None
return f"/guests?guest={guest.id}"
def get_guest_content_type():
return ContentType.objects.get_for_model(Guest)
class TaskLinkedGuestVisitSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
guest_visit = serializers.SerializerMethodField()
guest_visit_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"guest_visit_id",
"guest_visit",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_guest_visit(self, obj):
if obj.kind != TaskLinkedObjectKind.GUEST_VISIT:
return None
visit = getattr(obj, "content_object", None)
if visit is None:
return None
return {
"id": visit.id,
"guest": _guest_payload(visit.guest),
"inviter": EmployeeBriefSerializer(visit.inviter).data,
"inviter_snapshot_name": visit.inviter_snapshot_name,
"inviter_snapshot_email": visit.inviter_snapshot_email,
"purpose": visit.purpose,
"visit_comment": visit.visit_comment,
"status": visit.status,
"status_display": visit.get_status_display(),
"access_starts_at": visit.access_starts_at,
"access_expires_at": visit.access_expires_at,
"all_day": visit.all_day,
"unlimited": visit.unlimited,
"submitted_at": visit.submitted_at,
"is_active_now": visit.is_active_now,
"is_expired": visit.is_expired,
"created_at": visit.created_at,
"updated_at": visit.updated_at,
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
visit = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if obj.kind != TaskLinkedObjectKind.GUEST_VISIT or visit is None or not user:
return False
return user_can_access_guest_visit(user, visit)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
visit = getattr(obj, "content_object", None)
if visit is None or not self.get_can_open(obj):
return None
return f"/guests?visit={visit.id}"
def get_guest_visit_content_type():
return ContentType.objects.get_for_model(GuestVisit)
class TaskLinkedAttendanceRecordSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
attendance_record = serializers.SerializerMethodField()
attendance_record_id = serializers.IntegerField(source="object_id", read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
class Meta:
model = TaskLinkedObject
fields = [
"id",
"kind",
"attendance_record_id",
"attendance_record",
"can_open",
"object_url",
"created_by",
"created_at",
]
read_only_fields = fields
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_attendance_record(self, obj):
if obj.kind != TaskLinkedObjectKind.ATTENDANCE_RECORD:
return None
record = getattr(obj, "content_object", None)
if record is None:
return None
return {
"id": record.id,
"employee_id": record.employee_id,
"employee": EmployeeBriefSerializer(record.employee).data,
"date": record.date,
"display_name": record.display_name,
"arrival_time": record.arrival_time,
"departure_time": record.departure_time,
"work_hours": record.work_hours,
"expected_hours": record.expected_hours,
"is_workday": record.is_workday,
"effective_is_workday": record.effective_is_workday,
"is_late": record.is_late,
"late_minutes": record.late_minutes,
"is_early_leave": record.is_early_leave,
"early_leave_minutes": record.early_leave_minutes,
"is_underwork": record.is_underwork,
"underwork_hours": record.underwork_hours,
"is_overtime": record.is_overtime,
"overtime_hours": record.overtime_hours,
"is_absent": record.is_absent,
"statuses": record.statuses,
"employee_issues": record.employee_issues,
"technical_issues": record.technical_issues,
"personnel_status": record.personnel_status,
"personnel_status_label": record.personnel_status_label,
"comments_count": get_comment_count(record),
"updated_at": record.updated_at,
}
@extend_schema_field(serializers.BooleanField())
def get_can_open(self, obj):
record = getattr(obj, "content_object", None)
request = self.context.get("request")
user = getattr(request, "user", None)
if (
obj.kind != TaskLinkedObjectKind.ATTENDANCE_RECORD
or record is None
or not user
):
return False
return user_can_access_attendance_record(user, record)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
record = getattr(obj, "content_object", None)
if record is None or not self.get_can_open(obj):
return None
return f"/attendance?record={record.id}"
def get_attendance_record_content_type():
return ContentType.objects.get_for_model(AttendanceRecord)
def _linked_object_cover_payload(link, context):
source = getattr(link, "content_object", None)
kind = link.kind
title = link.get_kind_display()
description = ""
image_url = None
if source is not None:
if kind == TaskLinkedObjectKind.POST:
title = source.title or "Новость"
description = source.body or ""
if source.image:
from api.v1.utils import build_media_url
image_url = build_media_url(source.image, context.get("request"))
elif kind == TaskLinkedObjectKind.MESSAGE:
title = source.author.get_full_name() or source.author.email or "Сообщение"
description = source.content or ""
elif kind == TaskLinkedObjectKind.CALENDAR_EVENT:
title = getattr(source, "title", None) or "Календарное событие"
description = getattr(source, "description", "") or ""
elif kind == TaskLinkedObjectKind.DOCUMENT:
title = source.title or "Документ"
description = source.description or ""
elif kind == TaskLinkedObjectKind.REQUEST:
title = source.display_title or source.title or "Заявление"
description = source.comment or ""
elif kind == TaskLinkedObjectKind.PROCUREMENT_REQUEST:
title = source.title or "Заявка на закупку"
description = source.description or ""
elif kind == TaskLinkedObjectKind.EMPLOYEE:
title = source.get_full_name() or source.email or "Сотрудник"
description = getattr(source, "position_name", "") or ""
elif kind == TaskLinkedObjectKind.GUEST:
title = source.full_name or "Гость"
description = source.organization or ""
elif kind == TaskLinkedObjectKind.GUEST_VISIT:
title = source.guest.full_name or "Гостевой визит"
description = source.purpose or ""
elif kind == TaskLinkedObjectKind.ATTENDANCE_RECORD:
title = source.display_name or "Запись посещаемости"
description = str(source.date)
return {
"id": link.id,
"kind": kind,
"kind_display": link.get_kind_display(),
"object_id": link.object_id,
"title": str(title),
"description": str(description),
"image_url": image_url,
}
class TaskCoverSerializer(serializers.ModelSerializer):
kind_display = serializers.CharField(source="get_kind_display", read_only=True)
attachment = serializers.SerializerMethodField()
checklist = serializers.SerializerMethodField()
comment = serializers.SerializerMethodField()
external_link = serializers.SerializerMethodField()
linked_object = serializers.SerializerMethodField()
class Meta:
model = TaskCover
fields = [
"id",
"kind",
"kind_display",
"attachment",
"checklist",
"comment",
"external_link",
"linked_object",
"updated_at",
]
read_only_fields = fields
@extend_schema_field(serializers.DictField(allow_null=True))
def get_attachment(self, obj):
if obj.kind != TaskCoverType.ATTACHMENT or obj.attachment is None:
return None
return TaskAttachmentSerializer(
obj.attachment,
context=self.context,
).data
@extend_schema_field(serializers.DictField(allow_null=True))
def get_checklist(self, obj):
if obj.kind != TaskCoverType.CHECKLIST:
return None
items = list(obj.task.checklist_items.all())
return {
"total": len(items),
"completed": sum(item.is_completed for item in items),
"items": [
{
"id": item.id,
"title": item.title,
"is_completed": item.is_completed,
}
for item in items[:6]
],
}
@extend_schema_field(serializers.DictField(allow_null=True))
def get_comment(self, obj):
message = obj.comment
if obj.kind != TaskCoverType.COMMENT or message is None or message.is_deleted:
return None
attachments = [
attachment
for attachment in message.task_attachments.all()
if attachment.task_id == obj.task_id
]
return {
"id": message.id,
"author": EmployeeBriefSerializer(message.author).data,
"text": message.content,
"created_at": message.created_at,
"attachments": TaskAttachmentSerializer(
attachments,
many=True,
context=self.context,
).data,
}
@extend_schema_field(serializers.DictField(allow_null=True))
def get_external_link(self, obj):
if obj.kind != TaskCoverType.EXTERNAL_LINK or obj.external_link is None:
return None
return TaskExternalLinkSerializer(
obj.external_link,
context=self.context,
).data
@extend_schema_field(serializers.DictField(allow_null=True))
def get_linked_object(self, obj):
if obj.kind != TaskCoverType.LINKED_OBJECT or obj.linked_object is None:
return None
return _linked_object_cover_payload(obj.linked_object, self.context)