uchill/backend/apps/chat/models.py

504 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Модели для системы чата и сообщений.
"""
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}"