197 lines
7.6 KiB
Python

# backend/communications/models.py
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import IntegrityError, models
from django.db.models import Q
from django.utils import timezone
from employees.models import Department, EmployeeDepartment
Employee = get_user_model()
class ChatReadState(models.Model):
chat = models.ForeignKey(
"Chat", on_delete=models.CASCADE, related_name="read_states"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="chat_read_states",
)
last_read_at = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
# заменяем устаревший unique_together на UniqueConstraint
constraints = [
models.UniqueConstraint(
fields=["chat", "user"],
name="uniq_chatreadstate_chat_user",
),
]
indexes = [
models.Index(fields=["chat", "user"]),
models.Index(fields=["chat", "last_read_at"]),
]
def __str__(self):
return f"read:{self.user_id}@{self.chat_id} → {self.last_read_at or '-'}"
class Chat(models.Model):
CHAT_TYPE_CHOICES = [
("private", "Личный"),
("department", "Отдел"),
("global", "Глобальный"),
]
type = models.CharField(
max_length=16, choices=CHAT_TYPE_CHOICES, verbose_name="Тип чата", db_index=True
)
participants = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="chats",
verbose_name="Участники",
help_text="Используется только для личных чатов",
)
department = models.ForeignKey(
Department,
null=True,
blank=True,
on_delete=models.CASCADE, # при удалении отдела чат удалится
verbose_name="Отдел",
help_text="Указывается только для чатов отдела",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
is_main = models.BooleanField(default=False, verbose_name="Основной чат")
class Meta:
verbose_name = "Чат"
verbose_name_plural = "Чаты"
constraints = [
# Ровно один «главный» глобальный чат
models.UniqueConstraint(
fields=["type"],
condition=Q(is_main=True, type="global"),
name="unique_main_global_chat",
),
# Ровно один «главный» чат на отдел
models.UniqueConstraint(
fields=["type", "department"],
condition=Q(is_main=True, type="department"),
name="unique_main_department_chat",
),
]
indexes = [
models.Index(fields=["type", "is_main"]),
models.Index(fields=["department"]),
models.Index(fields=["created_at"]),
]
def clean(self):
super().clean()
# Доп. валидация только для глобального «главного»
if self.is_main and self.type == "global":
exists = (
Chat.objects.exclude(pk=self.pk)
.filter(type="global", is_main=True)
.exists()
)
if exists:
raise ValidationError("Основной глобальный чат уже существует.")
def delete(self, *args, **kwargs):
# Запрещаем удалять только глобальный основной чат
if self.is_main and self.type == "global":
raise ValidationError("Основной глобальный чат компании нельзя удалить!")
return super().delete(*args, **kwargs)
def mark_read(self, user):
"""
Помечает чат прочитанным «до последнего сообщения».
Сделано без update_or_create — устойчиво для SQLite.
"""
last = self.messages.order_by("-created_at").only("created_at").first()
ts = last.created_at if last else timezone.now()
# обновим, только если новое время больше
updated = ChatReadState.objects.filter(
chat=self, user=user, last_read_at__lt=ts
).update(last_read_at=ts)
if not updated:
try:
ChatReadState.objects.create(chat=self, user=user, last_read_at=ts)
except IntegrityError:
# гонка: запись уже есть — попробуем ещё раз обновить
ChatReadState.objects.filter(
chat=self, user=user, last_read_at__lt=ts
).update(last_read_at=ts)
def unread_count_for(self, user):
"""
Количество непрочитанных (кроме своих).
"""
rs = (
ChatReadState.objects.filter(chat=self, user=user)
.only("last_read_at")
.first()
)
qs = self.messages.exclude(author=user)
if rs and rs.last_read_at:
qs = qs.filter(created_at__gt=rs.last_read_at)
return qs.count()
@property
def get_participants(self):
"""
Возвращает QuerySet участников чата (по типу).
Для отдела берём активные связи + руководителя.
"""
if self.type == "private":
return self.participants.all()
if self.type == "department" and self.department_id:
employee_ids = EmployeeDepartment.objects.filter(
department_id=self.department_id, is_active=True
).values_list("employee_id", flat=True)
return Employee.objects.filter(
Q(id__in=employee_ids) | Q(id=self.department.head_id)
).distinct()
if self.type == "global":
return Employee.objects.filter(is_active=True)
return Employee.objects.none()
def __str__(self):
if self.type == "private":
# лениво формируем подпись, без лишних запросов
parts = list(self.participants.values_list("first_name", "last_name")[:3])
names = ", ".join(f"{ln} {fn}".strip() for fn, ln in parts) or "—"
more = self.participants.count() - len(parts)
if more > 0:
names += f" и ещё {more}"
return f"Личный чат: {names}"
if self.type == "department":
return f"Чат отдела: {self.department or '—'}"
return "Глобальный чат"
class Message(models.Model):
chat = models.ForeignKey(
Chat, related_name="messages", on_delete=models.CASCADE, verbose_name="Чат"
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Автор"
)
content = models.TextField("Текст сообщения")
created_at = models.DateTimeField(
auto_now_add=True, verbose_name="Время отправки", db_index=True
)
class Meta:
ordering = ["created_at"]
verbose_name = "Сообщение"
verbose_name_plural = "Сообщения"
def __str__(self):
return f"{self.author}: {self.content[:30]}"