385 lines
13 KiB
Python
385 lines
13 KiB
Python
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))
|