852 lines
29 KiB
Python

from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
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 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 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,
)
from tasks.models import (
TaskCover,
TaskCoverType,
TaskLinkedObject,
TaskLinkedObjectKind,
)
from .resources import TaskAttachmentSerializer, TaskExternalLinkSerializer
User = get_user_model()
class TaskLinkedObjectSerializer(serializers.ModelSerializer):
"""Shared access and navigation contract for linked task resources."""
created_by = EmployeeBriefSerializer(read_only=True)
can_open = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()
linked_kind = None
def _linked_source(self, obj):
if obj.kind != self.linked_kind:
return None
return getattr(obj, "content_object", None)
def _can_open_linked_object(self, obj, access_checker):
source = self._linked_source(obj)
request = self.context.get("request")
user = getattr(request, "user", None)
return bool(source is not None and user and access_checker(user, source))
def _linked_object_url(self, obj, url_builder):
source = self._linked_source(obj)
if source is None or not self.get_can_open(obj):
return None
return url_builder(source)
class TaskLinkedPostSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.POST
post = serializers.SerializerMethodField()
post_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_feed_post)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(obj, lambda source: f"/?post={source.id}")
def get_post_content_type():
return ContentType.objects.get_for_model(Post)
class TaskLinkedMessageSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.MESSAGE
message = serializers.SerializerMethodField()
message_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(
obj, lambda user, source: user_can_access_chat(source.chat, user)
)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj, lambda source: f"/messages/{source.chat_id}?message={source.id}"
)
def get_message_content_type():
return ContentType.objects.get_for_model(Message)
class TaskLinkedCalendarEventSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.CALENDAR_EVENT
event = serializers.SerializerMethodField()
event_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_calendar_event)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj,
lambda source: f"/calendar?calendar={source.calendar_id}&event={source.id}",
)
def get_calendar_event_content_type():
return ContentType.objects.get_for_model(Event)
class TaskLinkedDocumentSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.DOCUMENT
document = serializers.SerializerMethodField()
document_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_document)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj, lambda source: f"/documents?document={source.id}"
)
def get_document_content_type():
return ContentType.objects.get_for_model(Document)
class TaskLinkedRequestSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.REQUEST
request = serializers.SerializerMethodField()
request_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_employee_request)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj, lambda source: f"/requests?request={source.id}"
)
def get_request_content_type():
return ContentType.objects.get_for_model(EmployeeRequest)
class TaskLinkedProcurementRequestSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.PROCUREMENT_REQUEST
procurement_request = serializers.SerializerMethodField()
procurement_request_id = serializers.IntegerField(
source="object_id", read_only=True
)
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):
return self._can_open_linked_object(obj, user_can_access_procurement_request)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj, lambda source: f"/procurement?request={source.id}"
)
def get_procurement_request_content_type():
return ContentType.objects.get_for_model(ProcurementRequest)
class TaskLinkedEmployeeSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.EMPLOYEE
employee = serializers.SerializerMethodField()
employee_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_employee)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(obj, lambda source: f"/users/{source.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(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.GUEST
guest = serializers.SerializerMethodField()
guest_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_guest)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(obj, lambda source: f"/guests?guest={source.id}")
def get_guest_content_type():
return ContentType.objects.get_for_model(Guest)
class TaskLinkedGuestVisitSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.GUEST_VISIT
guest_visit = serializers.SerializerMethodField()
guest_visit_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_guest_visit)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(obj, lambda source: f"/guests?visit={source.id}")
def get_guest_visit_content_type():
return ContentType.objects.get_for_model(GuestVisit)
class TaskLinkedAttendanceRecordSerializer(TaskLinkedObjectSerializer):
linked_kind = TaskLinkedObjectKind.ATTENDANCE_RECORD
attendance_record = serializers.SerializerMethodField()
attendance_record_id = serializers.IntegerField(source="object_id", read_only=True)
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):
return self._can_open_linked_object(obj, user_can_access_attendance_record)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_object_url(self, obj):
return self._linked_object_url(
obj, lambda source: f"/attendance?record={source.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)