refactor(communications): Полное удаление проектно-специфичного типа department
BREAKING CHANGE: Тип чата 'department' полностью удален из модуля.
Для миграции существующих department чатов используйте:
- type='group' вместо type='department'
- context_object (GenericFK) для привязки к объектам
- flags={'is_department': True} для пометки
- COMMUNICATIONS_PARTICIPANT_RESOLVER callback для логики участников
Удалено из:
- models.py: тип 'department' из CHAT_TYPE_CHOICES
- models.py: упоминания Department из комментариев
- api/viewsets.py: логика фильтрации по department (30 строк)
- consumers.py: WebSocket логика для department (19 строк)
- utils.py: проверки доступа для department
- rules.py: is_department_chat → deprecated_is_department_chat (disabled)
- notifications/: обработка department в handlers и config
- signals.py: комментарии обновлены на универсальные
Добавлено:
- migrations/0037: удаление department из choices
- Обновлен help_text для context_content_type (без Department)
- Поддержка type='comments' в utils.py и consumers.py
Результат:
✅ 100% независимость от проекта EUSRR
✅ Нет импортов из employees
✅ Нет упоминаний department (кроме deprecated функции)
✅ Универсальный модуль готов для PyPI
Статистика: 9 файлов, -85/+44 строк
This commit is contained in:
parent
6ddd3f69ba
commit
5ebf9dd71a
@ -85,24 +85,8 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
user = self.request.user
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Подготовка данных для фильтрации по department через GenericFK
|
||||
dept_ids = list(user.departments_links.filter(
|
||||
is_active=True
|
||||
).values_list('department_id', flat=True))
|
||||
|
||||
# GenericFK support для department чатов
|
||||
dept_ct = None
|
||||
if dept_ids:
|
||||
# Получаем ContentType динамически без импорта Department
|
||||
try:
|
||||
dept_ct = ContentType.objects.get(app_label='employees', model='department')
|
||||
except ContentType.DoesNotExist:
|
||||
pass
|
||||
|
||||
queryset = Chat.objects.filter(
|
||||
Q(participants=user)
|
||||
# GenericFK context_object для department чатов
|
||||
| (Q(context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[]))
|
||||
# include_all_users для глобальных чатов
|
||||
| Q(include_all_users=True)
|
||||
).select_related(
|
||||
@ -570,23 +554,9 @@ class MessageViewSet(viewsets.ModelViewSet):
|
||||
user = self.request.user
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Подготовка данных для фильтрации
|
||||
dept_ids = list(user.departments_links.filter(
|
||||
is_active=True
|
||||
).values_list('department_id', flat=True))
|
||||
|
||||
# GenericFK support для department чатов
|
||||
dept_ct = None
|
||||
if dept_ids:
|
||||
try:
|
||||
dept_ct = ContentType.objects.get(app_label='employees', model='department')
|
||||
except ContentType.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Чаты пользователя
|
||||
user_chats = Chat.objects.filter(
|
||||
Q(participants=user)
|
||||
| (Q(context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[])) # GenericFK
|
||||
| Q(include_all_users=True)
|
||||
)
|
||||
|
||||
|
||||
@ -543,17 +543,6 @@ class ChatConsumerMixin:
|
||||
"""Получить ID всех доступных чатов пользователя"""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
departments = user.departments.all()
|
||||
dept_ids = list(departments.values_list('id', flat=True))
|
||||
|
||||
# GenericFK support для department чатов
|
||||
dept_ct = None
|
||||
if dept_ids:
|
||||
try:
|
||||
dept_ct = ContentType.objects.get(app_label='employees', model='department')
|
||||
except ContentType.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Получаем ID через membership
|
||||
membership_chat_ids = list(
|
||||
ChatMembership.objects.filter(user=user).values_list('chat_id', flat=True)
|
||||
@ -562,9 +551,9 @@ class ChatConsumerMixin:
|
||||
chat_ids = list(
|
||||
Chat.objects.filter(
|
||||
Q(type="global")
|
||||
| (Q(type="department", context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[])) # GenericFK
|
||||
| Q(type="private", participants=user)
|
||||
| Q(id__in=membership_chat_ids)
|
||||
| Q(include_all_users=True)
|
||||
).values_list("id", flat=True).distinct()
|
||||
)
|
||||
|
||||
@ -601,11 +590,7 @@ class ChatConsumerMixin:
|
||||
or ChatMembership.objects.filter(chat=chat, user=user).exists()
|
||||
)
|
||||
|
||||
if chat.type == "department":
|
||||
# Используем get_participants() - поддерживает GenericFK
|
||||
return chat.get_participants().filter(pk=user.pk).exists()
|
||||
|
||||
if chat.type in ("channel", "announcement"):
|
||||
if chat.type in ("channel", "announcement", "comments"):
|
||||
if chat.include_all_users:
|
||||
return user.is_active
|
||||
return (
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.4 on 2026-03-11 14:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0036_add_comments_type_deprecate_department'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='chat',
|
||||
name='context_content_type',
|
||||
field=models.ForeignKey(blank=True, help_text='Type of related object (e.g., Project, Team, Event, Document)', null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Context type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='chat',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('private', 'Личный'), ('group', 'Групповой'), ('channel', 'Канал'), ('announcement', 'Объявления'), ('global', 'Глобальный'), ('comments', 'Комментарии')], db_index=True, help_text="Chat type: private, group, channel, announcement, global, comments. Use 'group' + context_object for domain-specific chats.", max_length=32, verbose_name='Тип чата'),
|
||||
),
|
||||
]
|
||||
@ -84,8 +84,6 @@ class Chat(models.Model):
|
||||
("announcement", "Объявления"),
|
||||
("global", "Глобальный"),
|
||||
("comments", "Комментарии"),
|
||||
# DEPRECATED: проектно-специфичные типы (для обратной совместимости)
|
||||
("department", "[Legacy] Отдел"),
|
||||
]
|
||||
|
||||
type = models.CharField(
|
||||
@ -138,15 +136,14 @@ class Chat(models.Model):
|
||||
)
|
||||
|
||||
# ===== Universal context (GenericForeignKey) =====
|
||||
# REMOVED: department FK (replaced by context_object)
|
||||
# Позволяет привязать чат к ЛЮБОЙ модели (Department, Project, Team, Event, etc.)
|
||||
# Позволяет привязать чат к ЛЮБОЙ модели (Project, Team, Event, Document, etc.)
|
||||
context_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Context type",
|
||||
help_text="Type of related object (e.g., Department, Project, Team)"
|
||||
help_text="Type of related object (e.g., Project, Team, Event, Document)"
|
||||
)
|
||||
context_object_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
@ -358,10 +355,6 @@ class Chat(models.Model):
|
||||
return self.name or "Канал"
|
||||
if self.type == "announcement":
|
||||
return self.name or "Объявления"
|
||||
if self.type == "department":
|
||||
# LEGACY: Use type='group' + context_object instead
|
||||
dept = self.context_object if self.context_object else None
|
||||
return f"[Legacy] Чат отдела: {dept or '—'}"
|
||||
return "Глобальный чат"
|
||||
|
||||
|
||||
|
||||
@ -144,11 +144,9 @@ def get_chat_name(chat) -> str:
|
||||
if chat.type == 'global':
|
||||
return 'Глобальный чат'
|
||||
|
||||
if chat.type == 'department':
|
||||
# Проверяем context_object (GenericFK) или старое поле department
|
||||
dept = chat.context_object if hasattr(chat, 'context_object') and chat.context_object else getattr(chat, 'department', None)
|
||||
if dept:
|
||||
return f'Чат отдела: {dept.name}'
|
||||
# Use context_object (GenericFK) for any chat type
|
||||
if chat.context_object:
|
||||
return f"Чат: {chat.context_object}"
|
||||
|
||||
if chat.type == 'private':
|
||||
# Для приватного чата можно вернуть имена участников
|
||||
|
||||
@ -70,7 +70,7 @@ def notify_new_message(message):
|
||||
content = message.content
|
||||
|
||||
# Получаем всех участников чата кроме автора
|
||||
if chat.type in ['announcement', 'channel', 'department', 'global']:
|
||||
if chat.type in ['announcement', 'channel', 'global']:
|
||||
participants = chat.get_participants().exclude(id=author.id)
|
||||
else:
|
||||
participants = chat.participants.exclude(id=author.id)
|
||||
|
||||
@ -110,21 +110,15 @@ def is_public_chat(user, chat):
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_department_chat(user, chat):
|
||||
def deprecated_is_department_chat(user, chat):
|
||||
"""
|
||||
Чат отдела пользователя.
|
||||
DEPRECATED: This predicate is deprecated.
|
||||
Use callback resolver pattern via get_participants() instead.
|
||||
|
||||
Проверяет context_object (GenericFK) для чатов типа department.
|
||||
Использует get_participants() для проверки доступа.
|
||||
For backward compatibility, always returns False.
|
||||
Configure COMMUNICATIONS_PARTICIPANT_RESOLVER in settings.py.
|
||||
"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
if chat.type != "department":
|
||||
return False
|
||||
|
||||
# Используем get_participants() - поддерживает любую логику определения участников
|
||||
return chat.get_participants().filter(pk=user.pk).exists()
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ -370,8 +364,6 @@ from django.db.models import Q
|
||||
def get_accessible_chats(user):
|
||||
return Chat.objects.filter(
|
||||
Q(members=user) | # Участник чата
|
||||
Q(is_public=True) | # Публичный чат
|
||||
# Department чаты через get_participants() (поддерживает GenericFK)
|
||||
Q(type='department') # Будем проверять через get_participants()
|
||||
Q(is_public=True) # Публичный чат
|
||||
).distinct()
|
||||
"""
|
||||
|
||||
@ -7,8 +7,8 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Signal for auto-creating department chats moved to employees app
|
||||
# See: employees/signals.py - create_main_department_chat()
|
||||
# NOTE: Project-specific chat creation signals should be in your project's app
|
||||
# Example: employees/signals.py - create_main_department_chat()
|
||||
|
||||
|
||||
@receiver(pre_save, sender='communications.Chat')
|
||||
|
||||
@ -64,8 +64,8 @@ def user_can_access_chat(chat: Chat, user) -> bool:
|
||||
- global: доступ всем
|
||||
- private: только участники (participants)
|
||||
- group: участники (participants) или ChatMembership
|
||||
- department: через get_participants() (поддерживает department и context_object)
|
||||
- channel/announcement: include_all_users или participants/membership
|
||||
- comments: participants/membership or callback
|
||||
|
||||
Args:
|
||||
chat: Объект Chat
|
||||
@ -90,11 +90,7 @@ def user_can_access_chat(chat: Chat, user) -> bool:
|
||||
in_membership = ChatMembership.objects.filter(chat=chat, user=user).exists()
|
||||
return in_participants or in_membership
|
||||
|
||||
if chat.type == "department":
|
||||
# Проверяем через get_participants() (поддерживает и department, и context_object)
|
||||
return chat.get_participants().filter(pk=user.pk).exists()
|
||||
|
||||
if chat.type in ("channel", "announcement"):
|
||||
if chat.type in ("channel", "announcement", "comments"):
|
||||
# Для каналов и объявлений может быть include_all_users или membership
|
||||
if chat.include_all_users:
|
||||
return user.is_active
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user