1336 lines
46 KiB
Python
1336 lines
46 KiB
Python
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)
|