""" Serializers for Communications API """ from rest_framework import serializers from django.contrib.auth import get_user_model from ..models import ( Chat, Message, MessageAttachment, MessageReaction, Poll, PollOption, PollVote, ChatMembership, ChatUserSettings ) from ..serialization import serialize_message User = get_user_model() class ChatUserSettingsSerializer(serializers.ModelSerializer): """Настройки пользователя для чата""" class Meta: model = ChatUserSettings fields = ['is_pinned', 'notifications_enabled'] class ChatMembershipSerializer(serializers.ModelSerializer): """Членство в чате""" user_name = serializers.CharField(source='user.get_full_name', read_only=True) can_manage_members = serializers.BooleanField(read_only=True) class Meta: model = ChatMembership fields = [ 'id', 'user', 'user_name', 'role', 'joined_at', 'invited_by', 'is_active', 'left_at', 'can_send_messages', 'can_add_members', 'can_remove_members', 'can_pin_messages', 'can_manage_members' ] read_only_fields = ['joined_at', 'left_at'] class ChatListSerializer(serializers.ModelSerializer): """Сериализатор для списка чатов (облегченный)""" last_message = serializers.SerializerMethodField() unread_count = serializers.SerializerMethodField() participant_names = serializers.SerializerMethodField() member_ids = serializers.SerializerMethodField() is_pinned = serializers.SerializerMethodField() notifications_enabled = serializers.SerializerMethodField() last_read_message_id = serializers.SerializerMethodField() # NEW: context info context_type = serializers.CharField(source='context_content_type.model', read_only=True) class Meta: model = Chat fields = [ 'id', 'type', 'name', 'description', 'avatar', 'created_at', # NEW: universal fields 'context_object_id', 'context_type', 'flags', 'extra_data', 'include_all_users', # DEPRECATED: для обратной совместимости 'is_main', # Messages & status 'last_message', 'unread_count', 'participant_names', 'member_ids', 'is_pinned', 'notifications_enabled', 'last_read_message_id' ] def get_unread_count(self, obj): """ Количество непрочитанных из денормализованного поля. ОПТИМИЗИРОВАНО: Использует prefetch'нутый my_read_state вместо подзапроса. """ if hasattr(obj, 'my_read_state') and obj.my_read_state: return obj.my_read_state[0].unread_count return 0 def get_last_message(self, obj): """Последнее сообщение в чате""" last_msg = obj.messages.filter(is_deleted=False).order_by('-created_at').first() if last_msg: return { 'id': last_msg.id, 'content': last_msg.content[:100], 'author_name': last_msg.author.get_full_name() if last_msg.author else 'Unknown', 'created_at': last_msg.created_at.isoformat(), } return None def get_participant_names(self, obj): """Имена участников (для приватных чатов)""" if obj.type == 'private': active_members = obj.memberships.filter(is_active=True).select_related('user')[:5] return [m.user.get_full_name() for m in active_members] return [] def get_member_ids(self, obj): """ID участников (из prefetch, для поиска существующего чата)""" return list( obj.memberships.filter(is_active=True).values_list('user_id', flat=True) ) def get_is_pinned(self, obj): """Закреплен ли чат для текущего пользователя (из prefetch)""" if hasattr(obj, 'my_settings') and obj.my_settings: return obj.my_settings[0].is_pinned return False def get_notifications_enabled(self, obj): """Включены ли уведомления (из prefetch)""" if hasattr(obj, 'my_settings') and obj.my_settings: return obj.my_settings[0].notifications_enabled return True def get_last_read_message_id(self, obj): """ID последнего прочитанного сообщения (из prefetch)""" if hasattr(obj, 'my_read_state') and obj.my_read_state: return obj.my_read_state[0].last_read_message_id return None class ChatDetailSerializer(serializers.ModelSerializer): """Детальный сериализатор чата MIGRATION: Убрали participants, используем только memberships """ participant_details = serializers.SerializerMethodField() memberships = ChatMembershipSerializer(many=True, read_only=True) user_settings = serializers.SerializerMethodField() is_pinned = serializers.SerializerMethodField() notifications_enabled = serializers.SerializerMethodField() last_read_message_id = serializers.SerializerMethodField() # NEW: context info context_type = serializers.CharField(source='context_content_type.model', read_only=True, allow_null=True) context_app = serializers.CharField(source='context_content_type.app_label', read_only=True, allow_null=True) class Meta: model = Chat fields = [ 'id', 'type', 'name', 'description', 'avatar', 'created_by', 'created_at', # NEW: universal fields 'context_object_id', 'context_type', 'context_app', 'flags', 'extra_data', 'include_all_users', # DEPRECATED: для обратной совместимости (не удалять до полной миграции клиента) 'is_main', # MIGRATION: Убрали 'participants', используем только memberships 'participant_details', 'memberships', 'user_settings', 'is_pinned', 'notifications_enabled', 'last_read_message_id' ] read_only_fields = ['created_at', 'created_by', 'context_type', 'context_app'] def get_participant_details(self, obj): """Детали участников MIGRATION: Используем memberships вместо participants """ from django.contrib.auth import get_user_model User = get_user_model() # Получаем активных участников через memberships active_memberships = obj.memberships.filter(is_active=True).select_related('user')[:20] return [{ 'id': m.user.id, 'name': m.user.get_full_name(), 'avatar': m.user.avatar.url if m.user.avatar else None } for m in active_memberships] def get_user_settings(self, obj): """Настройки текущего пользователя для чата (из prefetch)""" if hasattr(obj, 'my_settings') and obj.my_settings: return ChatUserSettingsSerializer(obj.my_settings[0]).data return {'is_pinned': False, 'notifications_enabled': True} def get_is_pinned(self, obj): """Закреплен ли чат для текущего пользователя (из prefetch)""" if hasattr(obj, 'my_settings') and obj.my_settings: return obj.my_settings[0].is_pinned return False def get_notifications_enabled(self, obj): """Включены ли уведомления для текущего пользователя (из prefetch)""" if hasattr(obj, 'my_settings') and obj.my_settings: return obj.my_settings[0].notifications_enabled return True def get_last_read_message_id(self, obj): """ID последнего прочитанного сообщения (из prefetch)""" if hasattr(obj, 'my_read_state') and obj.my_read_state: return obj.my_read_state[0].last_read_message_id return None class MessageAttachmentSerializer(serializers.ModelSerializer): """Вложение сообщения""" class Meta: model = MessageAttachment fields = [ 'id', 'file_name', 'file_type', 'file_size', 'mime_type', 'file', 'thumbnail', 'uploaded_at', 'width', 'height' ] read_only_fields = ['uploaded_at', 'width', 'height'] class MessageReactionSerializer(serializers.ModelSerializer): """Реакция на сообщение""" user_name = serializers.CharField(source='user.get_full_name', read_only=True) class Meta: model = MessageReaction fields = ['id', 'emoji', 'user', 'user_name', 'created_at'] read_only_fields = ['created_at'] class PollOptionSerializer(serializers.ModelSerializer): """Опция голосования""" percentage = serializers.SerializerMethodField() voters = serializers.SerializerMethodField() class Meta: model = PollOption fields = ['id', 'text', 'position', 'vote_count', 'percentage', 'voters'] def get_percentage(self, obj): """Процент голосов""" if obj.poll and obj.poll.total_voters > 0: return round((obj.vote_count / obj.poll.total_voters) * 100, 1) return 0 def get_voters(self, obj): """Список проголосовавших (если не анонимное)""" if obj.poll and obj.poll.is_anonymous: return [] # Возвращаем список с именами votes = obj.votes.select_related('voter').all() return [ { 'voter__first_name': vote.voter.first_name, 'voter__last_name': vote.voter.last_name, 'voter__id': vote.voter.id } for vote in votes ] class PollSerializer(serializers.ModelSerializer): """Голосование""" options = PollOptionSerializer(many=True, read_only=True) user_voted_option_ids = serializers.SerializerMethodField() # Поля для создания (write_only) options_data = serializers.ListField( child=serializers.CharField(max_length=200), write_only=True, required=False, help_text="Список вариантов ответов (строки)" ) chat_id = serializers.IntegerField(write_only=True, required=True) correct_option_index = serializers.IntegerField(write_only=True, required=False, allow_null=True) closes_in_minutes = serializers.IntegerField(write_only=True, required=False, allow_null=True) class Meta: model = Poll fields = [ 'id', 'message', 'question', 'is_anonymous', 'is_multiple_choice', 'is_quiz', 'is_closed', 'closes_at', 'allows_custom_answers', 'total_voters', 'options', 'user_voted_option_ids', # write_only поля 'options_data', 'chat_id', 'correct_option_index', 'closes_in_minutes' ] read_only_fields = ['id', 'message', 'is_closed', 'total_voters'] def get_user_voted_option_ids(self, obj): """ID опций за которые проголосовал текущий пользователь""" request = self.context.get('request') if request and request.user.is_authenticated: votes = obj.votes.filter(voter=request.user) return [v.option_id for v in votes] return [] def create(self, validated_data): """Создание голосования с опциями""" from communications.models import Chat, Message from datetime import timedelta from django.utils import timezone import logging logger = logging.getLogger(__name__) logger.info(f"[PollSerializer.create] validated_data: {validated_data}") # Извлекаем данные для создания options_data = validated_data.pop('options_data', []) chat_id = validated_data.pop('chat_id') correct_option_index = validated_data.pop('correct_option_index', None) closes_in_minutes = validated_data.pop('closes_in_minutes', None) logger.info(f"[PollSerializer.create] options_data: {options_data}, chat_id: {chat_id}") # Получаем автора (может быть передан через perform_create или взять из запроса) author = validated_data.pop('author', None) or self.context.get('request').user # Создаем сообщение в чате try: chat = Chat.objects.get(id=chat_id) except Chat.DoesNotExist: logger.error(f"[PollSerializer.create] Chat {chat_id} not found") raise serializers.ValidationError({'chat_id': 'Chat not found'}) message = Message.objects.create( chat=chat, author=author, content=f"📊 {validated_data['question']}", is_system=False ) # Устанавливаем closes_at если указано if closes_in_minutes and closes_in_minutes > 0: validated_data['closes_at'] = timezone.now() + timedelta(minutes=closes_in_minutes) # Создаем голосование poll = Poll.objects.create( message=message, author=author, **validated_data ) # Создаем опции for index, option_text in enumerate(options_data): is_correct = (correct_option_index is not None and index == correct_option_index) poll.options.create( text=option_text, position=index, is_correct=is_correct ) # Отправляем WebSocket уведомление о новом сообщении from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from communications.serialization import serialize_message # Перезагружаем сообщение с poll и options для полной сериализации message.refresh_from_db() message_data = serialize_message(message) channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( f'chat_{chat.id}', { 'type': 'chat_message', 'chat_id': chat.id, 'payload': message_data } ) logger.info(f"[PollSerializer.create] WebSocket notification sent for message {message.id}") return poll class MessageListSerializer(serializers.ModelSerializer): """Облегченный сериализатор для списка сообщений""" author_name = serializers.CharField(source='author.get_full_name', read_only=True) author_avatar = serializers.SerializerMethodField() reactions_summary = serializers.SerializerMethodField() class Meta: model = Message fields = [ 'id', 'content', 'author', 'author_name', 'author_avatar', 'created_at', 'is_edited', 'edited_at', 'is_deleted', 'is_pinned', 'has_attachments', 'reactions_summary' ] def get_author_avatar(self, obj): if obj.author and obj.author.avatar: return obj.author.avatar.url return None def get_reactions_summary(self, obj): """Суммарная информация о реакциях""" reactions = {} for reaction in obj.reactions.all(): emoji = reaction.emoji if emoji not in reactions: reactions[emoji] = {'count': 0, 'users': [], 'user_names': []} reactions[emoji]['count'] += 1 reactions[emoji]['users'].append(reaction.user_id) reactions[emoji]['user_names'].append(reaction.user.get_full_name()) return reactions class MessageDetailSerializer(serializers.ModelSerializer): """Детальный сериализатор сообщения (использует функцию serialize_message)""" class Meta: model = Message fields = '__all__' def to_representation(self, instance): """Используем существующую функцию сериализации""" return serialize_message(instance) class MessageCreateSerializer(serializers.ModelSerializer): """Сериализатор для создания/редактирования сообщений""" class Meta: model = Message fields = [ 'content', 'chat', 'reply_to', 'is_forwarded' ] def validate(self, attrs): """Валидация: сообщение должно иметь либо текст, либо вложения""" content = attrs.get('content', '').strip() # Проверка контента будет на уровне view при наличии файлов return attrs def create(self, validated_data): """Создание сообщения с автором""" validated_data['author'] = self.context['request'].user return super().create(validated_data) class MessageEditSerializer(serializers.Serializer): """Сериализатор для редактирования сообщения""" content = serializers.CharField(required=False, allow_blank=True) existing_attachment_ids = serializers.ListField( child=serializers.IntegerField(), required=False, allow_empty=True ) class ReactionSerializer(serializers.Serializer): """Сериализатор для добавления/удаления реакции""" emoji = serializers.CharField(max_length=10) class ForwardMessageSerializer(serializers.Serializer): """Сериализатор для пересылки сообщений""" message_ids = serializers.ListField( child=serializers.IntegerField(), min_length=1 ) target_chat_id = serializers.IntegerField() class BulkDeleteSerializer(serializers.Serializer): """Сериализатор для массового удаления""" message_ids = serializers.ListField( child=serializers.IntegerField(), min_length=1 )