197 lines
7.6 KiB
Python
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]}"
|