2026-09-07 19:43:48 +03:00

385 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from api.v1.employees.serializers import EmployeeBriefSerializer
from api.v1.employees.serializers.department import DepartmentBriefSerializer
from employees.models import Department
from tasks.group_ranking import visible_group_position_map
from tasks.models import (
TaskBoard,
TaskBoardAccessScope,
TaskBoardGroup,
TaskBoardGroupMembership,
TaskBoardPlacement,
TaskColumnRow,
TaskLabel,
TaskUserSettings,
)
from .structure import (
TaskColumnRowSerializer,
TaskColumnSerializer,
TaskLabelSerializer,
)
from .tasks import TaskSerializer
User = get_user_model()
class TaskBoardSerializer(serializers.ModelSerializer):
created_by = EmployeeBriefSerializer(read_only=True)
members = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
many=True,
required=False,
)
member_details = EmployeeBriefSerializer(
source="members",
many=True,
read_only=True,
)
departments = serializers.PrimaryKeyRelatedField(
queryset=Department.objects.all(),
many=True,
required=False,
)
department_details = DepartmentBriefSerializer(
source="departments",
many=True,
read_only=True,
)
columns = TaskColumnSerializer(many=True, read_only=True)
column_rows = serializers.SerializerMethodField()
tasks = TaskSerializer(many=True, read_only=True)
labels = serializers.SerializerMethodField()
tasks_count = serializers.IntegerField(read_only=True, default=0)
is_default_for_current_user = serializers.SerializerMethodField()
group_ids = serializers.SerializerMethodField()
group_id = serializers.SerializerMethodField()
group_positions = serializers.SerializerMethodField()
group_sort_keys = serializers.SerializerMethodField()
is_pinned = serializers.BooleanField(
source="current_user_is_pinned",
read_only=True,
default=False,
)
notifications_enabled = serializers.BooleanField(
source="current_user_notifications_enabled",
read_only=True,
default=True,
)
view_state = serializers.SerializerMethodField()
class Meta:
model = TaskBoard
fields = [
"id",
"name",
"description",
"avatar",
"access_scope",
"created_by",
"members",
"member_details",
"departments",
"department_details",
"is_archived",
"columns",
"column_rows",
"tasks",
"labels",
"tasks_count",
"is_default_for_current_user",
"group_ids",
"group_id",
"group_positions",
"group_sort_keys",
"is_pinned",
"notifications_enabled",
"view_state",
"created_at",
"updated_at",
]
@extend_schema_field(TaskLabelSerializer(many=True))
def get_labels(self, obj):
labels = getattr(self, "_global_labels_cache", None)
if labels is None:
labels = list(TaskLabel.objects.order_by("name", "id"))
self._global_labels_cache = labels
return TaskLabelSerializer(labels, many=True).data
def _get_group_memberships(self, obj):
memberships = getattr(obj, "current_user_group_memberships", None)
if memberships is None:
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not user.is_authenticated:
return []
memberships = list(
TaskBoardGroupMembership.objects.filter(
board=obj,
)
.filter(
Q(group__user=user) | Q(group__is_shared=True),
)
.exclude(
group__hidden_by=user,
)
.select_related("group")
)
return memberships
@extend_schema_field(serializers.ListField(child=serializers.IntegerField()))
def get_group_ids(self, obj):
return sorted(
membership.group_id for membership in self._get_group_memberships(obj)
)
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_group_id(self, obj):
group_ids = self.get_group_ids(obj)
return group_ids[0] if group_ids else None
@extend_schema_field(
serializers.DictField(child=serializers.IntegerField(min_value=1))
)
def get_group_positions(self, obj):
position_map = getattr(self, "_group_position_map_cache", None)
if position_map is None:
request = self.context.get("request")
user = getattr(request, "user", None)
position_map = (
visible_group_position_map(user)
if user and user.is_authenticated
else {}
)
self._group_position_map_cache = position_map
return {
str(membership.group_id): position_map[(membership.group_id, obj.id)]
for membership in self._get_group_memberships(obj)
if (membership.group_id, obj.id) in position_map
}
@extend_schema_field(serializers.DictField(child=serializers.CharField()))
def get_group_sort_keys(self, obj):
return {
str(membership.group_id): membership.rank_key
for membership in self._get_group_memberships(obj)
}
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 and "members" in attrs:
existing_member_ids = (
set(self.instance.members.values_list("id", flat=True))
if self.instance is not None
else set()
)
if any(
(not member.is_active or member.is_hidden_in_ui)
and member.id not in existing_member_ids
for member in attrs["members"]
):
raise serializers.ValidationError(
{
"members": (
"Добавлять на доску можно только доступных "
"активных сотрудников."
)
}
)
if access_scope != TaskBoardAccessScope.RESTRICTED:
attrs["members"] = []
attrs["departments"] = []
return attrs
@extend_schema_field(serializers.DictField())
def get_view_state(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return {
"hidden_column_ids": [],
"collapsed_base_column_ids": [],
"collapsed_row_ids": [],
}
raw_state = (
TaskBoardPlacement.objects.filter(user=user, board=obj)
.values_list("view_state", flat=True)
.first()
or {}
)
valid_column_ids = set(
obj.columns.filter(parent__isnull=True, is_archived=False).values_list(
"id", flat=True
)
)
valid_row_ids = set(
TaskColumnRow.objects.filter(
column__board=obj,
column__is_archived=False,
).values_list("id", flat=True)
)
def valid_ids(key, allowed_ids):
values = raw_state.get(key, []) if isinstance(raw_state, dict) else []
if not isinstance(values, list):
return []
return sorted(
{
value
for value in values
if isinstance(value, int) and value in allowed_ids
}
)
return {
"hidden_column_ids": valid_ids("hidden_column_ids", valid_column_ids),
"collapsed_base_column_ids": valid_ids(
"collapsed_base_column_ids", valid_column_ids
),
"collapsed_row_ids": valid_ids("collapsed_row_ids", valid_row_ids),
}
@extend_schema_field(TaskColumnRowSerializer(many=True))
def get_column_rows(self, obj):
rows = (
TaskColumnRow.objects.filter(
column__board=obj,
)
.annotate(tasks_count=Count("tasks", distinct=True))
.order_by("column_id", "position", "id")
)
return TaskColumnRowSerializer(rows, many=True).data
@extend_schema_field(serializers.BooleanField())
def get_is_default_for_current_user(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return False
settings_obj = getattr(user, "task_settings", None)
return bool(settings_obj and settings_obj.default_board_id == obj.id)
class TaskBoardSummarySerializer(TaskBoardSerializer):
open_tasks_count = serializers.IntegerField(read_only=True, default=0)
completed_tasks_count = serializers.IntegerField(read_only=True, default=0)
class Meta(TaskBoardSerializer.Meta):
fields = [
"id",
"name",
"description",
"avatar",
"access_scope",
"created_by",
"member_details",
"department_details",
"tasks_count",
"open_tasks_count",
"completed_tasks_count",
"is_default_for_current_user",
"group_ids",
"group_id",
"group_positions",
"group_sort_keys",
"is_pinned",
"notifications_enabled",
"created_at",
"updated_at",
]
class TaskBoardGroupSerializer(serializers.ModelSerializer):
boards_count = serializers.IntegerField(read_only=True, default=0)
is_default_for_current_user = serializers.SerializerMethodField()
is_hidden = serializers.SerializerMethodField()
is_owner = serializers.SerializerMethodField()
class Meta:
model = TaskBoardGroup
fields = [
"id",
"name",
"position",
"is_shared",
"boards_count",
"is_default_for_current_user",
"is_hidden",
"is_owner",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def validate_name(self, value):
name = value.strip()
if not name:
raise serializers.ValidationError("Укажите название группы.")
request = self.context.get("request")
owner_id = (
self.instance.user_id
if self.instance
else getattr(getattr(request, "user", None), "id", None)
)
queryset = TaskBoardGroup.objects.filter(user_id=owner_id, name__iexact=name)
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
if queryset.exists():
raise serializers.ValidationError(
"Группа с таким названием уже существует."
)
return name
@extend_schema_field(serializers.BooleanField())
def get_is_default_for_current_user(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return False
if not hasattr(self, "_default_group_id"):
self._default_group_id = (
TaskUserSettings.objects.filter(user=user)
.values_list("default_group_id", flat=True)
.first()
)
return self._default_group_id == obj.id
@extend_schema_field(serializers.BooleanField())
def get_is_hidden(self, obj):
hidden_by = getattr(obj, "current_user_hidden_by", None)
if hidden_by is not None:
return bool(hidden_by)
request = self.context.get("request")
user = getattr(request, "user", None)
return bool(
user
and getattr(user, "is_authenticated", False)
and obj.hidden_by.filter(id=user.id).exists()
)
@extend_schema_field(serializers.BooleanField())
def get_is_owner(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
return bool(user and obj.user_id == getattr(user, "id", None))