892 lines
30 KiB
Python
892 lines
30 KiB
Python
"""
|
||
Serializers for Communications API
|
||
"""
|
||
|
||
from datetime import timedelta
|
||
|
||
import rules
|
||
from drf_spectacular.types import OpenApiTypes
|
||
from drf_spectacular.utils import extend_schema_field
|
||
from django.contrib.auth import get_user_model
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
from rest_framework import serializers
|
||
|
||
from ..models import (
|
||
Chat,
|
||
Message,
|
||
MessageAttachment,
|
||
MessageReaction,
|
||
Poll,
|
||
PollOption,
|
||
ChatMembership,
|
||
ChatUserSettings,
|
||
)
|
||
from ..mentions import build_message_mentions
|
||
from ..serialization import serialize_message, serialize_poll
|
||
from ..utils import user_can_access_chat
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
def _get_active_memberships(obj, *, limit: int | None = None):
|
||
if hasattr(obj, "_prefetched_objects_cache") and "memberships" in obj._prefetched_objects_cache:
|
||
memberships = list(obj.memberships.all())
|
||
else:
|
||
memberships = list(
|
||
obj.memberships.filter(is_active=True, user__is_active=True).select_related(
|
||
"user"
|
||
)
|
||
)
|
||
memberships = [
|
||
membership
|
||
for membership in memberships
|
||
if membership.is_active and membership.user.is_active
|
||
]
|
||
|
||
if limit is not None:
|
||
memberships = memberships[:limit]
|
||
|
||
return memberships
|
||
|
||
|
||
def _get_last_message(obj):
|
||
if hasattr(obj, "_prefetched_last_message"):
|
||
prefetched = obj._prefetched_last_message
|
||
return prefetched[0] if prefetched else None
|
||
|
||
return (
|
||
obj.messages.filter(is_deleted=False)
|
||
.select_related("author", "poll")
|
||
.prefetch_related("attachments")
|
||
.order_by("-created_at")
|
||
.first()
|
||
)
|
||
|
||
|
||
def _get_last_message_preview(message):
|
||
content = (message.content or "").strip()
|
||
if content:
|
||
return content[:100], "text"
|
||
|
||
poll = getattr(message, "poll", None)
|
||
if poll is not None:
|
||
question = (poll.question or "").strip()
|
||
preview = f"Голосование: {question}" if question else "Голосование"
|
||
return preview[:100], "poll"
|
||
|
||
attachments = list(message.attachments.all())
|
||
if len(attachments) > 1:
|
||
return f"Вложения: {len(attachments)}", "attachments"
|
||
if attachments:
|
||
attachment_type = attachments[0].file_type
|
||
labels = {
|
||
"image": "Фото",
|
||
"video": "Видео",
|
||
"audio": "Аудио",
|
||
"file": "Файл",
|
||
}
|
||
return labels.get(attachment_type, "Файл"), attachment_type or "file"
|
||
|
||
return "Сообщение", "message"
|
||
|
||
|
||
def _serialize_last_message(message):
|
||
preview_text, preview_type = _get_last_message_preview(message)
|
||
return {
|
||
"id": message.id,
|
||
"content": (message.content or "")[:100],
|
||
"preview_text": preview_text,
|
||
"preview_type": preview_type,
|
||
"author_name": (
|
||
message.author.get_full_name() if message.author else "Unknown"
|
||
),
|
||
"created_at": message.created_at.isoformat(),
|
||
}
|
||
|
||
|
||
class ChatLastMessageSerializer(serializers.Serializer):
|
||
id = serializers.IntegerField()
|
||
content = serializers.CharField(allow_blank=True)
|
||
preview_text = serializers.CharField()
|
||
preview_type = serializers.CharField()
|
||
author_name = serializers.CharField()
|
||
created_at = serializers.DateTimeField()
|
||
|
||
|
||
class ChatParticipantPreviewSerializer(serializers.Serializer):
|
||
id = serializers.IntegerField()
|
||
name = serializers.CharField()
|
||
avatar = serializers.CharField(allow_null=True)
|
||
|
||
|
||
class PollVoterSerializer(serializers.Serializer):
|
||
voter__first_name = serializers.CharField()
|
||
voter__last_name = serializers.CharField()
|
||
voter__id = serializers.IntegerField()
|
||
|
||
|
||
class ChatUserSettingsSerializer(serializers.ModelSerializer):
|
||
"""Настройки пользователя для чата"""
|
||
|
||
notifications_enabled = serializers.SerializerMethodField()
|
||
effective_notification_mode = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = ChatUserSettings
|
||
fields = [
|
||
"is_pinned",
|
||
"notifications_enabled",
|
||
"notification_mode",
|
||
"effective_notification_mode",
|
||
"muted_until",
|
||
"show_notification_preview",
|
||
"notification_sound_enabled",
|
||
]
|
||
|
||
def get_notifications_enabled(self, obj):
|
||
return (
|
||
obj.effective_notification_mode()
|
||
== ChatUserSettings.NotificationMode.ALL
|
||
)
|
||
|
||
def get_effective_notification_mode(self, obj):
|
||
return obj.effective_notification_mode()
|
||
|
||
|
||
class ChatNotificationPreferencesUpdateSerializer(serializers.Serializer):
|
||
notification_mode = serializers.ChoiceField(
|
||
choices=ChatUserSettings.NotificationMode.choices,
|
||
required=False,
|
||
)
|
||
mute_for_seconds = serializers.IntegerField(
|
||
min_value=0,
|
||
max_value=31_536_000,
|
||
required=False,
|
||
)
|
||
muted_until = serializers.DateTimeField(
|
||
allow_null=True,
|
||
required=False,
|
||
)
|
||
show_notification_preview = serializers.BooleanField(required=False)
|
||
notification_sound_enabled = serializers.BooleanField(required=False)
|
||
|
||
|
||
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()
|
||
has_unread_mention = serializers.SerializerMethodField()
|
||
participant_details = 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",
|
||
"has_unread_mention",
|
||
"participant_details",
|
||
"participant_names",
|
||
"member_ids",
|
||
"is_pinned",
|
||
"notifications_enabled",
|
||
"last_read_message_id",
|
||
]
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
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
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_has_unread_mention(self, obj):
|
||
if hasattr(obj, "my_read_state") and obj.my_read_state:
|
||
return obj.my_read_state[0].unread_mention_count > 0
|
||
return False
|
||
|
||
@extend_schema_field(ChatLastMessageSerializer)
|
||
def get_last_message(self, obj):
|
||
"""Последнее сообщение в чате (использует prefetch если доступен)"""
|
||
last_msg = _get_last_message(obj)
|
||
if last_msg:
|
||
return _serialize_last_message(last_msg)
|
||
return None
|
||
|
||
@extend_schema_field(ChatParticipantPreviewSerializer(many=True))
|
||
def get_participant_details(self, obj):
|
||
members = _get_active_memberships(obj, limit=20)
|
||
return [
|
||
{
|
||
"id": membership.user.id,
|
||
"name": membership.user.get_full_name(),
|
||
"avatar": membership.user.avatar.url if membership.user.avatar else None,
|
||
}
|
||
for membership in members
|
||
]
|
||
|
||
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
|
||
def get_participant_names(self, obj):
|
||
"""Имена участников (для приватных чатов, из prefetch)"""
|
||
if obj.type == "private":
|
||
members = _get_active_memberships(obj, limit=5)
|
||
return [membership.user.get_full_name() for membership in members]
|
||
return []
|
||
|
||
@extend_schema_field(
|
||
serializers.ListField(child=serializers.IntegerField())
|
||
)
|
||
def get_member_ids(self, obj):
|
||
"""ID участников (из prefetch, для поиска существующего чата)"""
|
||
return [membership.user_id for membership in _get_active_memberships(obj)]
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
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
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_notifications_enabled(self, obj):
|
||
"""Включены ли уведомления (из prefetch)"""
|
||
if hasattr(obj, "my_settings") and obj.my_settings:
|
||
return (
|
||
obj.my_settings[0].effective_notification_mode()
|
||
== ChatUserSettings.NotificationMode.ALL
|
||
)
|
||
return True
|
||
|
||
@extend_schema_field(serializers.IntegerField(allow_null=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()
|
||
last_message = serializers.SerializerMethodField()
|
||
unread_count = serializers.SerializerMethodField()
|
||
has_unread_mention = serializers.SerializerMethodField()
|
||
context_content_type = serializers.PrimaryKeyRelatedField(
|
||
queryset=ContentType.objects.all(),
|
||
write_only=True,
|
||
required=False,
|
||
allow_null=True,
|
||
)
|
||
include_all_employees = serializers.BooleanField(
|
||
write_only=True, required=False
|
||
)
|
||
|
||
# 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_content_type",
|
||
"context_object_id",
|
||
"context_type",
|
||
"context_app",
|
||
"flags",
|
||
"extra_data",
|
||
"include_all_users",
|
||
"include_all_employees",
|
||
# DEPRECATED: для обратной совместимости
|
||
# (не удалять до полной миграции клиента)
|
||
"is_main",
|
||
# MIGRATION: Убрали 'participants', используем только memberships
|
||
"participant_details",
|
||
"memberships",
|
||
"user_settings",
|
||
"is_pinned",
|
||
"notifications_enabled",
|
||
"last_read_message_id",
|
||
"last_message",
|
||
"unread_count",
|
||
"has_unread_mention",
|
||
]
|
||
read_only_fields = [
|
||
"created_at",
|
||
"created_by",
|
||
"context_type",
|
||
"context_app",
|
||
]
|
||
|
||
def create(self, validated_data):
|
||
include_all_employees = validated_data.pop(
|
||
"include_all_employees", None
|
||
)
|
||
if include_all_employees is not None:
|
||
validated_data["include_all_users"] = include_all_employees
|
||
return super().create(validated_data)
|
||
|
||
def update(self, instance, validated_data):
|
||
include_all_employees = validated_data.pop(
|
||
"include_all_employees", None
|
||
)
|
||
if include_all_employees is not None:
|
||
validated_data["include_all_users"] = include_all_employees
|
||
return super().update(instance, validated_data)
|
||
|
||
@extend_schema_field(ChatParticipantPreviewSerializer(many=True))
|
||
def get_participant_details(self, obj):
|
||
"""Детали участников
|
||
MIGRATION: Используем memberships вместо participants
|
||
"""
|
||
return [
|
||
{
|
||
"id": membership.user.id,
|
||
"name": membership.user.get_full_name(),
|
||
"avatar": membership.user.avatar.url if membership.user.avatar else None,
|
||
}
|
||
for membership in _get_active_memberships(obj, limit=20)
|
||
]
|
||
|
||
@extend_schema_field(ChatUserSettingsSerializer)
|
||
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,
|
||
"notification_mode": ChatUserSettings.NotificationMode.ALL,
|
||
"effective_notification_mode": ChatUserSettings.NotificationMode.ALL,
|
||
"muted_until": None,
|
||
"show_notification_preview": True,
|
||
"notification_sound_enabled": True,
|
||
}
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
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
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_notifications_enabled(self, obj):
|
||
"""Включены ли уведомления для текущего пользователя (из prefetch)"""
|
||
if hasattr(obj, "my_settings") and obj.my_settings:
|
||
return (
|
||
obj.my_settings[0].effective_notification_mode()
|
||
== ChatUserSettings.NotificationMode.ALL
|
||
)
|
||
return True
|
||
|
||
@extend_schema_field(serializers.IntegerField(allow_null=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
|
||
|
||
@extend_schema_field(ChatLastMessageSerializer)
|
||
def get_last_message(self, obj):
|
||
last_msg = _get_last_message(obj)
|
||
if not last_msg:
|
||
return None
|
||
|
||
return _serialize_last_message(last_msg)
|
||
|
||
@extend_schema_field(serializers.IntegerField())
|
||
def get_unread_count(self, obj):
|
||
if hasattr(obj, "my_read_state") and obj.my_read_state:
|
||
return obj.my_read_state[0].unread_count
|
||
return 0
|
||
|
||
@extend_schema_field(serializers.BooleanField())
|
||
def get_has_unread_mention(self, obj):
|
||
if hasattr(obj, "my_read_state") and obj.my_read_state:
|
||
return obj.my_read_state[0].unread_mention_count > 0
|
||
return False
|
||
|
||
|
||
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",
|
||
]
|
||
|
||
@extend_schema_field(serializers.FloatField())
|
||
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
|
||
|
||
@extend_schema_field(PollVoterSerializer(many=True))
|
||
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=True,
|
||
min_length=2,
|
||
max_length=10,
|
||
help_text="Список вариантов ответов (строки)",
|
||
)
|
||
chat_id = serializers.IntegerField(write_only=True, required=True)
|
||
reply_to_id = serializers.IntegerField(write_only=True, required=False)
|
||
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,
|
||
min_value=1,
|
||
max_value=525_600,
|
||
)
|
||
|
||
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",
|
||
"reply_to_id",
|
||
"correct_option_index",
|
||
"closes_in_minutes",
|
||
]
|
||
read_only_fields = ["id", "message", "is_closed", "total_voters"]
|
||
|
||
@extend_schema_field(
|
||
serializers.ListField(child=serializers.IntegerField())
|
||
)
|
||
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 to_representation(self, instance):
|
||
request = self.context.get("request")
|
||
user = request.user if request and request.user.is_authenticated else None
|
||
return serialize_poll(instance, user=user)
|
||
|
||
def validate_question(self, value):
|
||
question = value.strip()
|
||
if not question:
|
||
raise serializers.ValidationError("Введите вопрос")
|
||
return question
|
||
|
||
def validate(self, attrs):
|
||
request = self.context.get("request")
|
||
if not request or not request.user.is_authenticated:
|
||
raise serializers.ValidationError("Требуется авторизация")
|
||
|
||
try:
|
||
chat = Chat.objects.get(pk=attrs["chat_id"])
|
||
except Chat.DoesNotExist:
|
||
raise serializers.ValidationError(
|
||
{"chat_id": "Чат не найден"}
|
||
) from None
|
||
|
||
if not user_can_access_chat(chat, request.user):
|
||
raise serializers.ValidationError(
|
||
{"chat_id": "Нет доступа к этому чату"}
|
||
)
|
||
if not rules.test_rule(
|
||
"communications.send_message",
|
||
request.user,
|
||
chat,
|
||
):
|
||
raise serializers.ValidationError(
|
||
{"chat_id": "Нет права отправлять сообщения в этот чат"}
|
||
)
|
||
|
||
normalized_options = [option.strip() for option in attrs["options_data"]]
|
||
if any(not option for option in normalized_options):
|
||
raise serializers.ValidationError(
|
||
{"options_data": "Варианты не могут быть пустыми"}
|
||
)
|
||
if len({option.casefold() for option in normalized_options}) != len(
|
||
normalized_options
|
||
):
|
||
raise serializers.ValidationError(
|
||
{"options_data": "Варианты не должны повторяться"}
|
||
)
|
||
attrs["options_data"] = normalized_options
|
||
|
||
correct_option_index = attrs.get("correct_option_index")
|
||
if attrs.get("is_quiz"):
|
||
if attrs.get("is_multiple_choice"):
|
||
raise serializers.ValidationError(
|
||
{"is_multiple_choice": "Викторина допускает только один ответ"}
|
||
)
|
||
if correct_option_index is None or not (
|
||
0 <= correct_option_index < len(normalized_options)
|
||
):
|
||
raise serializers.ValidationError(
|
||
{"correct_option_index": "Выберите правильный вариант"}
|
||
)
|
||
elif correct_option_index is not None:
|
||
raise serializers.ValidationError(
|
||
{"correct_option_index": "Правильный ответ доступен только для викторины"}
|
||
)
|
||
|
||
if attrs.get("allows_custom_answers"):
|
||
raise serializers.ValidationError(
|
||
{"allows_custom_answers": "Свои варианты ответа пока не поддерживаются"}
|
||
)
|
||
|
||
self._reply_to = None
|
||
reply_to_id = attrs.get("reply_to_id")
|
||
if reply_to_id is not None:
|
||
try:
|
||
self._reply_to = chat.messages.get(
|
||
pk=reply_to_id,
|
||
is_deleted=False,
|
||
)
|
||
except Message.DoesNotExist:
|
||
raise serializers.ValidationError(
|
||
{"reply_to_id": "Исходное сообщение не найдено"}
|
||
) from None
|
||
|
||
self._poll_chat = chat
|
||
return attrs
|
||
|
||
def create(self, validated_data):
|
||
"""Создание голосования с опциями"""
|
||
options_data = validated_data.pop("options_data")
|
||
validated_data.pop("chat_id")
|
||
validated_data.pop("reply_to_id", None)
|
||
correct_option_index = validated_data.pop("correct_option_index", None)
|
||
closes_in_minutes = validated_data.pop("closes_in_minutes", None)
|
||
author = (
|
||
validated_data.pop("author", None)
|
||
or self.context.get("request").user
|
||
)
|
||
|
||
if closes_in_minutes:
|
||
validated_data["closes_at"] = timezone.now() + timedelta(
|
||
minutes=closes_in_minutes
|
||
)
|
||
|
||
with transaction.atomic():
|
||
message = Message.objects.create(
|
||
chat=self._poll_chat,
|
||
author=author,
|
||
content=validated_data["question"],
|
||
reply_to=self._reply_to,
|
||
is_system=False,
|
||
)
|
||
poll = Poll.objects.create(
|
||
message=message,
|
||
author=author,
|
||
**validated_data,
|
||
)
|
||
PollOption.objects.bulk_create(
|
||
[
|
||
PollOption(
|
||
poll=poll,
|
||
text=option_text,
|
||
position=index,
|
||
is_correct=(index == correct_option_index),
|
||
)
|
||
for index, option_text in enumerate(options_data)
|
||
]
|
||
)
|
||
|
||
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",
|
||
"mentions",
|
||
"author",
|
||
"author_name",
|
||
"author_avatar",
|
||
"created_at",
|
||
"is_edited",
|
||
"edited_at",
|
||
"is_deleted",
|
||
"is_pinned",
|
||
"has_attachments",
|
||
"reactions_summary",
|
||
]
|
||
|
||
@extend_schema_field(serializers.CharField(allow_null=True))
|
||
def get_author_avatar(self, obj):
|
||
if obj.author and obj.author.avatar:
|
||
return obj.author.avatar.url
|
||
return None
|
||
|
||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||
def get_reactions_summary(self, obj):
|
||
"""Суммарная информация о реакциях"""
|
||
return obj.get_reactions_summary()
|
||
|
||
|
||
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):
|
||
"""Сериализатор для создания/редактирования сообщений"""
|
||
|
||
mentioned_user_ids = serializers.ListField(
|
||
child=serializers.IntegerField(min_value=1),
|
||
write_only=True,
|
||
required=False,
|
||
allow_empty=True,
|
||
max_length=50,
|
||
)
|
||
|
||
class Meta:
|
||
model = Message
|
||
fields = [
|
||
"content",
|
||
"chat",
|
||
"reply_to",
|
||
"is_forwarded",
|
||
"mentioned_user_ids",
|
||
]
|
||
|
||
def validate(self, attrs):
|
||
"""Валидация: сообщение должно иметь либо текст, либо вложения"""
|
||
# Проверка контента будет на уровне view при наличии файлов
|
||
return attrs
|
||
|
||
def create(self, validated_data):
|
||
"""Создание сообщения с автором"""
|
||
author = self.context["request"].user
|
||
mentioned_user_ids = validated_data.pop("mentioned_user_ids", [])
|
||
validated_data["author"] = author
|
||
validated_data["mentions"] = build_message_mentions(
|
||
validated_data["chat"],
|
||
author,
|
||
validated_data.get("content", ""),
|
||
mentioned_user_ids,
|
||
)
|
||
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
|
||
)
|
||
mentioned_user_ids = serializers.ListField(
|
||
child=serializers.IntegerField(min_value=1),
|
||
required=False,
|
||
allow_empty=True,
|
||
max_length=50,
|
||
)
|
||
|
||
|
||
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
|
||
)
|