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))