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)