""" Модели для системы чата и сообщений. """ from django.db import models from django.utils import timezone import uuid import os def message_file_upload_path(instance, filename): """Путь для загрузки файлов сообщений.""" ext = filename.split('.')[-1] new_filename = f"{uuid.uuid4()}.{ext}" return os.path.join('file_chat', str(instance.message.chat.id), new_filename) class Chat(models.Model): """ Модель чата. """ CHAT_TYPE_CHOICES = [ ('direct', 'Личный'), ('group', 'Групповой'), ] # Основная информация uuid = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, verbose_name='UUID' ) chat_type = models.CharField( max_length=20, choices=CHAT_TYPE_CHOICES, default='direct', verbose_name='Тип чата', db_index=True ) name = models.CharField( max_length=255, blank=True, verbose_name='Название' ) description = models.TextField( blank=True, verbose_name='Описание' ) avatar = models.ImageField( upload_to='chat/avatars/', blank=True, null=True, verbose_name='Аватар' ) # Создатель created_by = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, related_name='created_chats', null=True, verbose_name='Создатель' ) # Связь с занятием (опционально) lesson = models.ForeignKey( 'schedule.Lesson', on_delete=models.SET_NULL, related_name='chats', null=True, blank=True, verbose_name='Занятие' ) # Статистика messages_count = models.IntegerField( default=0, verbose_name='Количество сообщений' ) last_message_at = models.DateTimeField( null=True, blank=True, verbose_name='Последнее сообщение', db_index=True ) # Настройки is_archived = models.BooleanField( default=False, verbose_name='Архивирован', db_index=True ) # Временные метки created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата создания' ) updated_at = models.DateTimeField( auto_now=True, verbose_name='Дата обновления' ) class Meta: db_table = 'chats' verbose_name = 'Чат' verbose_name_plural = 'Чаты' ordering = ['-last_message_at', '-created_at'] indexes = [ models.Index(fields=['chat_type']), models.Index(fields=['is_archived']), models.Index(fields=['last_message_at']), ] def __str__(self): if self.name: return self.name return f"Чат {self.uuid}" def update_last_message(self): """Обновить время последнего сообщения.""" self.last_message_at = timezone.now() self.save(update_fields=['last_message_at']) def increment_messages_count(self): """Увеличить счетчик сообщений.""" self.messages_count += 1 self.save(update_fields=['messages_count']) def get_participants_ids(self): """Получить ID участников чата.""" return list(self.participants.values_list('user_id', flat=True)) class ChatParticipant(models.Model): """ Модель участника чата. """ ROLE_CHOICES = [ ('admin', 'Администратор'), ('member', 'Участник'), ] chat = models.ForeignKey( Chat, on_delete=models.CASCADE, related_name='participants', verbose_name='Чат' ) user = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='chat_participations', verbose_name='Пользователь' ) role = models.CharField( max_length=20, choices=ROLE_CHOICES, default='member', verbose_name='Роль' ) # Статистика unread_count = models.IntegerField( default=0, verbose_name='Непрочитанных' ) last_read_at = models.DateTimeField( null=True, blank=True, verbose_name='Последнее прочтение' ) # Настройки is_muted = models.BooleanField( default=False, verbose_name='Уведомления отключены' ) is_pinned = models.BooleanField( default=False, verbose_name='Закреплен' ) # Временные метки joined_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата присоединения' ) left_at = models.DateTimeField( null=True, blank=True, verbose_name='Дата выхода' ) class Meta: db_table = 'chat_participants' verbose_name = 'Участник чата' verbose_name_plural = 'Участники чата' unique_together = ['chat', 'user'] ordering = ['-joined_at'] indexes = [ models.Index(fields=['user', 'chat']), models.Index(fields=['chat', 'user']), ] def __str__(self): return f"{self.user.email} в {self.chat}" def mark_as_read(self): """Отметить все сообщения как прочитанные.""" self.unread_count = 0 self.last_read_at = timezone.now() self.save() def increment_unread(self): """Увеличить счетчик непрочитанных.""" self.unread_count += 1 self.save(update_fields=['unread_count']) class Message(models.Model): """ Модель сообщения. """ MESSAGE_TYPE_CHOICES = [ ('text', 'Текст'), ('file', 'Файл'), ('image', 'Изображение'), ('video', 'Видео'), ('audio', 'Аудио'), ('system', 'Системное'), ] # Основная информация uuid = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, verbose_name='UUID' ) chat = models.ForeignKey( Chat, on_delete=models.CASCADE, related_name='messages', verbose_name='Чат' ) sender = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, related_name='sent_messages', null=True, verbose_name='Отправитель' ) message_type = models.CharField( max_length=20, choices=MESSAGE_TYPE_CHOICES, default='text', verbose_name='Тип сообщения' ) # Контент content = models.TextField( verbose_name='Содержимое' ) # Ответ на сообщение reply_to = models.ForeignKey( 'self', on_delete=models.SET_NULL, related_name='replies', null=True, blank=True, verbose_name='Ответ на' ) # Редактирование is_edited = models.BooleanField( default=False, verbose_name='Отредактировано' ) edited_at = models.DateTimeField( null=True, blank=True, verbose_name='Дата редактирования' ) # Удаление is_deleted = models.BooleanField( default=False, verbose_name='Удалено', db_index=True ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name='Дата удаления' ) # Временные метки created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата отправки', db_index=True ) class Meta: db_table = 'messages' verbose_name = 'Сообщение' verbose_name_plural = 'Сообщения' ordering = ['-created_at'] indexes = [ models.Index(fields=['chat', 'created_at']), models.Index(fields=['sender']), models.Index(fields=['is_deleted']), ] def __str__(self): preview = self.content[:50] if len(self.content) > 50 else self.content return f"{self.sender.email if self.sender else 'System'}: {preview}" def save(self, *args, **kwargs): """Переопределяем save.""" is_new = self.pk is None super().save(*args, **kwargs) # При создании нового сообщения if is_new: # Обновляем счетчик и время последнего сообщения в чате self.chat.increment_messages_count() self.chat.update_last_message() # Системные сообщения (уведомления) не увеличивают счётчик непрочитанных — уведомления есть отдельно if self.message_type != 'system': # Увеличиваем счетчик непрочитанных для всех участников кроме отправителя participants = list(self.chat.participants.exclude(user=self.sender)) for participant in participants: participant.unread_count += 1 if participants: ChatParticipant.objects.bulk_update(participants, ['unread_count']) def mark_as_edited(self): """Отметить как отредактированное.""" self.is_edited = True self.edited_at = timezone.now() self.save() def soft_delete(self): """Мягкое удаление.""" self.is_deleted = True self.deleted_at = timezone.now() self.save() class MessageFile(models.Model): """ Модель файла сообщения. """ message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='files', verbose_name='Сообщение' ) file = models.FileField( upload_to=lambda instance, filename: os.path.join('file_chat', str(instance.message.chat.id), filename), max_length=500, verbose_name='Файл' ) file_name = models.CharField( max_length=255, verbose_name='Имя файла' ) file_size = models.BigIntegerField( verbose_name='Размер файла (bytes)' ) file_type = models.CharField( max_length=100, verbose_name='MIME тип' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата загрузки' ) class Meta: db_table = 'message_files' verbose_name = 'Файл сообщения' verbose_name_plural = 'Файлы сообщений' ordering = ['created_at'] def __str__(self): return f"{self.file_name} ({self.message.uuid})" class MessageRead(models.Model): """ Модель прочтения сообщений. Отслеживает кто и когда прочитал сообщение. """ message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='reads', verbose_name='Сообщение' ) user = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='message_reads', verbose_name='Пользователь' ) read_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата прочтения', db_index=True ) class Meta: db_table = 'message_reads' verbose_name = 'Прочтение сообщения' verbose_name_plural = 'Прочтения сообщений' unique_together = ['message', 'user'] ordering = ['-read_at'] indexes = [ models.Index(fields=['message', 'user']), models.Index(fields=['user', 'read_at']), ] def __str__(self): return f"{self.user.email} прочитал {self.message.uuid}" class MessageReaction(models.Model): """ Модель реакции на сообщение. """ message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='reactions', verbose_name='Сообщение' ) user = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='message_reactions', verbose_name='Пользователь' ) emoji = models.CharField( max_length=10, verbose_name='Эмодзи' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата', db_index=True ) class Meta: db_table = 'message_reactions' verbose_name = 'Реакция на сообщение' verbose_name_plural = 'Реакции на сообщения' unique_together = ['message', 'user', 'emoji'] ordering = ['-created_at'] indexes = [ models.Index(fields=['message']), models.Index(fields=['user']), ] def __str__(self): return f"{self.user.email} - {self.emoji}"