504 lines
14 KiB
Python
504 lines
14 KiB
Python
"""
|
||
Модели для системы чата и сообщений.
|
||
"""
|
||
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()
|
||
|
||
# Увеличиваем счетчик непрочитанных для всех участников кроме отправителя
|
||
# Оптимизация: используем bulk_update вместо цикла с save()
|
||
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}"
|