from urllib.parse import urlsplit from django.db.models import 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.models import ( Task, TaskActivity, TaskAttachment, TaskBoard, TaskBoardAccessScope, TaskChecklistItem, TaskColumn, TaskExternalLink, TaskLabel, TaskLinkedObject, TaskLinkedObjectKind, ) 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", "board", "name", "color"] read_only_fields = ["id"] def validate_board(self, board): return validate_board_access(self, board) class TaskColumnSerializer(serializers.ModelSerializer): tasks_count = serializers.IntegerField(read_only=True, default=0) class Meta: model = TaskColumn fields = [ "id", "board", "name", "position", "color", "is_done", "is_archived", "tasks_count", "created_at", "updated_at", ] read_only_fields = [ "id", "created_by", "member_details", "department_details", "columns", "labels", "tasks", "tasks_count", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at", "tasks_count"] def validate_board(self, board): return validate_board_access(self, board) class TaskSerializer(serializers.ModelSerializer): created_by = EmployeeBriefSerializer(read_only=True) assignee = EmployeeBriefSerializer(read_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) 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() checklist_total = serializers.SerializerMethodField() checklist_completed = serializers.SerializerMethodField() class Meta: model = Task fields = [ "id", "board", "board_name", "column", "column_name", "column_color", "title", "description", "created_by", "assignee", "assignee_id", "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", "checklist_total", "checklist_completed", "created_at", "updated_at", ] read_only_fields = [ "id", "created_by", "board_name", "column_name", "column_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", "checklist_total", "checklist_completed", "created_at", "updated_at", ] @extend_schema_field(serializers.IntegerField()) def get_linked_posts_count(self, obj): if hasattr(obj, "linked_posts_count"): return obj.linked_posts_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.POST, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_messages_count(self, obj): if hasattr(obj, "linked_messages_count"): return obj.linked_messages_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.MESSAGE, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_events_count(self, obj): if hasattr(obj, "linked_events_count"): return obj.linked_events_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.CALENDAR_EVENT, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_documents_count(self, obj): if hasattr(obj, "linked_documents_count"): return obj.linked_documents_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.DOCUMENT, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_requests_count(self, obj): if hasattr(obj, "linked_requests_count"): return obj.linked_requests_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.REQUEST, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_procurement_requests_count(self, obj): if hasattr(obj, "linked_procurement_requests_count"): return obj.linked_procurement_requests_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_employees_count(self, obj): if hasattr(obj, "linked_employees_count"): return obj.linked_employees_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.EMPLOYEE, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_guests_count(self, obj): if hasattr(obj, "linked_guests_count"): return obj.linked_guests_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.GUEST, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_guest_visits_count(self, obj): if hasattr(obj, "linked_guest_visits_count"): return obj.linked_guest_visits_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.GUEST_VISIT, ).count() @extend_schema_field(serializers.IntegerField()) def get_linked_attendance_records_count(self, obj): if hasattr(obj, "linked_attendance_records_count"): return obj.linked_attendance_records_count return obj.linked_objects.filter( kind=TaskLinkedObjectKind.ATTENDANCE_RECORD, ).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() @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) labels = attrs.get("labels") 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 labels is not None and board: invalid_labels = [ label.id for label in labels if label.board_id != board.id ] if invalid_labels: raise serializers.ValidationError( {"label_ids": "Метки должны принадлежать выбранной доске."} ) return attrs def create(self, validated_data): labels = validated_data.pop("labels", []) if "position" not in validated_data: column = validated_data["column"] max_position = ( Task.objects.filter(column=column).aggregate(Max("position"))[ "position__max" ] or 0 ) validated_data["position"] = max_position + 1000 task = Task.objects.create(**validated_data) if labels: task.labels.set(labels) return task def update(self, instance, validated_data): labels = validated_data.pop("labels", None) task = super().update(instance, validated_data) 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) labels = TaskLabelSerializer(many=True, read_only=True) tasks = TaskSerializer(many=True, read_only=True) tasks_count = serializers.IntegerField(read_only=True, default=0) is_default_for_current_user = serializers.SerializerMethodField() class Meta: model = TaskBoard fields = [ "id", "name", "description", "access_scope", "created_by", "members", "member_details", "departments", "department_details", "is_archived", "columns", "labels", "tasks", "tasks_count", "is_default_for_current_user", "created_at", "updated_at", ] 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.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 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() class Meta: model = TaskAttachment fields = [ "id", "task", "file_name", "file_size", "mime_type", "uploaded_by", "created_at", "download_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 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)