feat(communications): Refactor Chat model to universal GenericForeignKey
BREAKING CHANGES (backward compatible with data migration): - Add context_object (GenericFK) to replace department FK - Add flags (JSONField) to replace is_main boolean - Add extra_data (JSONField) for extensibility - Rename include_all_employees → include_all_users NEW FEATURES: - Generic context support (Department, Project, Team, etc.) - Flexible flags system without migrations - Settings-based participant resolver callback - Zero data loss migrations (4 migrations) MIGRATIONS: - 0031: Add new fields (context_object, flags, extra_data) - 0032: Migrate department → context_object (15 chats) - 0033: Migrate is_main → flags['is_primary'] (15 chats) - 0034: Rename include_all_employees → include_all_users FILES CHANGED: - models.py: Refactored Chat.get_participants() with callback support - employees/utils.py: Added resolve_chat_participants() callback - settings.py: Added COMMUNICATIONS_PARTICIPANT_RESOLVER setting - verify_chat_migration.py: Command to verify data integrity TESTED: ✅ All 32 chats migrated successfully ✅ Legacy department FK still works ✅ Context-based chats work (GenericFK) ✅ Global/private/announcement chats work ✅ Participants resolver callback works
This commit is contained in:
parent
ec6189b96c
commit
51aecacf09
@ -0,0 +1,200 @@
|
||||
"""
|
||||
Команда для проверки корректности миграции модели Chat.
|
||||
|
||||
Использование:
|
||||
python manage.py verify_chat_migration
|
||||
|
||||
Проверяет:
|
||||
1. Миграцию department FK → context_object GFK
|
||||
2. Миграцию is_main Boolean → flags['is_primary'] JSON
|
||||
3. Переименование include_all_employees → include_all_users
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from communications.models import Chat
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Verify Chat migration: old fields → new fields'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
help='Show detailed information for each chat',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = options.get('verbose', False)
|
||||
|
||||
self.stdout.write("\n" + "=" * 70)
|
||||
self.stdout.write(self.style.HTTP_INFO("ПРОВЕРКА МИГРАЦИИ МОДЕЛИ CHAT"))
|
||||
self.stdout.write("=" * 70 + "\n")
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# ===== 1. Проверка department → context_object =====
|
||||
self.stdout.write(self.style.HTTP_INFO(
|
||||
"1. Проверка миграции department → context_object..."
|
||||
))
|
||||
|
||||
# Проверяем наличие старого поля (может быть удалено)
|
||||
has_department_field = hasattr(Chat, 'department')
|
||||
|
||||
if has_department_field:
|
||||
from employees.models import Department
|
||||
dept_ct = ContentType.objects.get_for_model(Department)
|
||||
chats_with_dept = Chat.objects.filter(department__isnull=False)
|
||||
count_dept = chats_with_dept.count()
|
||||
|
||||
self.stdout.write(f" Найдено чатов с department: {count_dept}")
|
||||
|
||||
for chat in chats_with_dept:
|
||||
# Проверяем что context заполнен
|
||||
if not chat.context_object_id:
|
||||
errors.append(
|
||||
f"Chat #{chat.id}: есть department но context_object_id is NULL"
|
||||
)
|
||||
elif chat.context_object_id != chat.department_id:
|
||||
errors.append(
|
||||
f"Chat #{chat.id}: context_object_id ({chat.context_object_id}) "
|
||||
f"!= department_id ({chat.department_id})"
|
||||
)
|
||||
elif chat.context_content_type_id != dept_ct.id:
|
||||
errors.append(
|
||||
f"Chat #{chat.id}: context_content_type не Department"
|
||||
)
|
||||
elif verbose:
|
||||
self.stdout.write(
|
||||
f" ✓ Chat #{chat.id}: department={chat.department_id} "
|
||||
f"→ context={chat.context_object_id}"
|
||||
)
|
||||
|
||||
if not errors and count_dept > 0:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✓ Все {count_dept} чатов с department мигрированы корректно"
|
||||
))
|
||||
elif count_dept == 0:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
" ⚠ Чатов с department не найдено"
|
||||
))
|
||||
else:
|
||||
# Поле department удалено - проверяем только context
|
||||
chats_with_context = Chat.objects.filter(
|
||||
context_content_type__isnull=False,
|
||||
context_object_id__isnull=False
|
||||
).count()
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✓ Поле 'department' удалено, чатов с context: {chats_with_context}"
|
||||
))
|
||||
|
||||
# ===== 2. Проверка is_main → flags['is_primary'] =====
|
||||
self.stdout.write("\n" + self.style.HTTP_INFO(
|
||||
"2. Проверка миграции is_main → flags['is_primary']..."
|
||||
))
|
||||
|
||||
has_is_main_field = hasattr(Chat, 'is_main')
|
||||
|
||||
if has_is_main_field:
|
||||
main_chats = Chat.objects.filter(is_main=True)
|
||||
count_main = main_chats.count()
|
||||
|
||||
self.stdout.write(f" Найдено главных чатов (is_main=True): {count_main}")
|
||||
|
||||
for chat in main_chats:
|
||||
flags_primary = chat.flags.get('is_primary') if chat.flags else False
|
||||
|
||||
if not flags_primary:
|
||||
errors.append(
|
||||
f"Chat #{chat.id}: is_main=True но flags['is_primary'] не установлен"
|
||||
)
|
||||
elif verbose:
|
||||
self.stdout.write(
|
||||
f" ✓ Chat #{chat.id}: is_main=True → flags['is_primary']=True"
|
||||
)
|
||||
|
||||
if not errors and count_main > 0:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✓ Все {count_main} главных чатов мигрированы в flags корректно"
|
||||
))
|
||||
elif count_main == 0:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
" ⚠ Главных чатов не найдено"
|
||||
))
|
||||
else:
|
||||
# Поле is_main удалено - проверяем только flags
|
||||
chats_with_primary = Chat.objects.filter(
|
||||
flags__is_primary=True
|
||||
).count()
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✓ Поле 'is_main' удалено, чатов с flags.is_primary: {chats_with_primary}"
|
||||
))
|
||||
|
||||
# ===== 3. Проверка include_all_users =====
|
||||
self.stdout.write("\n" + self.style.HTTP_INFO(
|
||||
"3. Проверка поля include_all_users..."
|
||||
))
|
||||
|
||||
has_old_field = hasattr(Chat, 'include_all_employees')
|
||||
has_new_field = hasattr(Chat, 'include_all_users')
|
||||
|
||||
if has_old_field and not has_new_field:
|
||||
warnings.append("Поле 'include_all_employees' еще не переименовано")
|
||||
count = Chat.objects.filter(include_all_employees=True).count()
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f" ⚠ Старое поле 'include_all_employees' существует, чатов: {count}"
|
||||
))
|
||||
elif has_new_field:
|
||||
count = Chat.objects.filter(include_all_users=True).count()
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f" ✓ Поле 'include_all_users' существует, чатов использующих: {count}"
|
||||
))
|
||||
else:
|
||||
errors.append("Ни 'include_all_employees' ни 'include_all_users' не найдены!")
|
||||
|
||||
# ===== 4. Общая статистика =====
|
||||
self.stdout.write("\n" + self.style.HTTP_INFO("4. Общая статистика..."))
|
||||
|
||||
total = Chat.objects.count()
|
||||
with_context = Chat.objects.filter(
|
||||
context_content_type__isnull=False
|
||||
).count()
|
||||
with_flags_primary = Chat.objects.filter(
|
||||
flags__is_primary=True
|
||||
).count()
|
||||
|
||||
self.stdout.write(f" Всего чатов: {total}")
|
||||
self.stdout.write(f" С context_object: {with_context}")
|
||||
self.stdout.write(f" С flags.is_primary: {with_flags_primary}")
|
||||
|
||||
# ===== ИТОГОВЫЙ ОТЧЕТ =====
|
||||
self.stdout.write("\n" + "=" * 70)
|
||||
|
||||
if errors:
|
||||
self.stdout.write(self.style.ERROR(f"❌ ОШИБОК: {len(errors)}"))
|
||||
for error in errors:
|
||||
self.stdout.write(self.style.ERROR(f" • {error}"))
|
||||
result_code = 1
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS("✅ ОШИБОК НЕ ОБНАРУЖЕНО"))
|
||||
result_code = 0
|
||||
|
||||
if warnings:
|
||||
self.stdout.write("\n" + self.style.WARNING(f"⚠️ ПРЕДУПРЕЖДЕНИЙ: {len(warnings)}"))
|
||||
for warning in warnings:
|
||||
self.stdout.write(self.style.WARNING(f" • {warning}"))
|
||||
|
||||
self.stdout.write("=" * 70 + "\n")
|
||||
|
||||
if result_code == 0:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
"✅ Миграция выполнена корректно! Данные не потеряны."
|
||||
))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
"❌ Обнаружены проблемы при миграции. Проверьте ошибки выше."
|
||||
))
|
||||
|
||||
return result_code
|
||||
@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.2.4 on 2026-03-11 10:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0030_chatreadstate_unread_count_and_more'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('employees', '0041_alter_departmentpermission_code'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='context_content_type',
|
||||
field=models.ForeignKey(blank=True, help_text='Type of related object (e.g., Department, Project, Team)', null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Context type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='context_object_id',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Context object ID'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='extra_data',
|
||||
field=models.JSONField(blank=True, default=dict, help_text='Additional chat metadata (extensible)', verbose_name='Extra data'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='flags',
|
||||
field=models.JSONField(blank=True, default=dict, help_text="Custom flags: {'is_primary': true, 'is_archived': false, etc.}", verbose_name='Flags'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='chat',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('private', 'Личный'), ('group', 'Групповой'), ('department', 'Отдел'), ('channel', 'Канал'), ('announcement', 'Объявления'), ('global', 'Глобальный')], db_index=True, help_text='Chat type: private, group, channel, announcement, department, global', max_length=32, verbose_name='Тип чата'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='chat',
|
||||
index=models.Index(fields=['context_content_type', 'context_object_id'], name='chat_context_idx'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,68 @@
|
||||
# Generated by Django 5.2.4 on 2026-03-11 10:07
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_department_to_context(apps, schema_editor):
|
||||
"""
|
||||
Копирует department → context_object для всех чатов с department.
|
||||
"""
|
||||
Chat = apps.get_model('communications', 'Chat')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
# Получаем ContentType для модели Department
|
||||
try:
|
||||
dept_ct = ContentType.objects.get(app_label='employees', model='department')
|
||||
except ContentType.DoesNotExist:
|
||||
print("ContentType для Department не найден, пропускаем миграцию")
|
||||
return
|
||||
|
||||
# Находим все чаты с department
|
||||
chats_with_dept = Chat.objects.filter(department__isnull=False)
|
||||
|
||||
count = 0
|
||||
for chat in chats_with_dept:
|
||||
chat.context_content_type = dept_ct
|
||||
chat.context_object_id = chat.department_id
|
||||
chat.save(update_fields=['context_content_type', 'context_object_id'])
|
||||
count += 1
|
||||
|
||||
print(f"Мигрировано {count} чатов: department → context_object")
|
||||
|
||||
|
||||
def reverse_migrate(apps, schema_editor):
|
||||
"""
|
||||
Откат: копирует context_object → department для Department объектов.
|
||||
"""
|
||||
Chat = apps.get_model('communications', 'Chat')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
try:
|
||||
dept_ct = ContentType.objects.get(app_label='employees', model='department')
|
||||
except ContentType.DoesNotExist:
|
||||
return
|
||||
|
||||
chats_with_context = Chat.objects.filter(
|
||||
context_content_type=dept_ct,
|
||||
context_object_id__isnull=False
|
||||
)
|
||||
|
||||
count = 0
|
||||
for chat in chats_with_context:
|
||||
chat.department_id = chat.context_object_id
|
||||
chat.save(update_fields=['department'])
|
||||
count += 1
|
||||
|
||||
print(f"Откат: восстановлено {count} чатов: context_object → department")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0031_add_universal_context_fields'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_department_to_context, reverse_code=reverse_migrate),
|
||||
]
|
||||
@ -0,0 +1,51 @@
|
||||
# Generated by Django 5.2.4 on 2026-03-11 10:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_is_main_to_flags(apps, schema_editor):
|
||||
"""
|
||||
Копирует is_main → flags['is_primary'].
|
||||
"""
|
||||
Chat = apps.get_model('communications', 'Chat')
|
||||
|
||||
chats_with_is_main = Chat.objects.filter(is_main=True)
|
||||
|
||||
count = 0
|
||||
for chat in chats_with_is_main:
|
||||
if not chat.flags:
|
||||
chat.flags = {}
|
||||
chat.flags['is_primary'] = True
|
||||
chat.save(update_fields=['flags'])
|
||||
count += 1
|
||||
|
||||
print(f"Мигрировано {count} чатов: is_main=True → flags['is_primary']=True")
|
||||
|
||||
|
||||
def reverse_migrate(apps, schema_editor):
|
||||
"""
|
||||
Откат: копирует flags['is_primary'] → is_main.
|
||||
"""
|
||||
Chat = apps.get_model('communications', 'Chat')
|
||||
|
||||
all_chats = Chat.objects.all()
|
||||
|
||||
count = 0
|
||||
for chat in all_chats:
|
||||
if chat.flags and chat.flags.get('is_primary'):
|
||||
chat.is_main = True
|
||||
chat.save(update_fields=['is_main'])
|
||||
count += 1
|
||||
|
||||
print(f"Откат: восстановлено {count} чатов: flags['is_primary'] → is_main")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0032_migrate_department_to_context'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_is_main_to_flags, reverse_code=reverse_migrate),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.4 on 2026-03-11 10:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0033_migrate_is_main_to_flags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='chat',
|
||||
old_name='include_all_employees',
|
||||
new_name='include_all_users',
|
||||
),
|
||||
]
|
||||
@ -1,10 +1,14 @@
|
||||
# backend/communications/models.py
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
# DEPRECATED: Will be removed after migration complete
|
||||
from employees.models import Department, EmployeeDepartment
|
||||
|
||||
Employee = get_user_model()
|
||||
@ -86,10 +90,11 @@ class Chat(models.Model):
|
||||
]
|
||||
|
||||
type = models.CharField(
|
||||
max_length=16,
|
||||
max_length=32, # Увеличено с 16 для кастомных типов
|
||||
choices=CHAT_TYPE_CHOICES,
|
||||
verbose_name="Тип чата",
|
||||
db_index=True
|
||||
db_index=True,
|
||||
help_text="Chat type: private, group, channel, announcement, department, global"
|
||||
)
|
||||
|
||||
# Новые базовые поля
|
||||
@ -119,9 +124,9 @@ class Chat(models.Model):
|
||||
)
|
||||
|
||||
# Для гибкой настройки участников
|
||||
include_all_employees = models.BooleanField(
|
||||
include_all_users = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Включить всех сотрудников",
|
||||
verbose_name="Включить всех пользователей",
|
||||
help_text="Для анонсов и общих чатов"
|
||||
)
|
||||
|
||||
@ -140,9 +145,44 @@ class Chat(models.Model):
|
||||
verbose_name="Отдел",
|
||||
help_text="Указывается только для чатов отдела",
|
||||
)
|
||||
|
||||
# ===== NEW: Universal context (GenericForeignKey) =====
|
||||
# Позволяет привязать чат к ЛЮБОЙ модели (Department, Project, Team, Event, 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)"
|
||||
)
|
||||
context_object_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Context object ID"
|
||||
)
|
||||
context_object = GenericForeignKey(
|
||||
'context_content_type',
|
||||
'context_object_id'
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
|
||||
is_main = models.BooleanField(default=False, verbose_name="Основной чат")
|
||||
|
||||
# ===== NEW: Flexible flags & metadata =====
|
||||
flags = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
verbose_name="Flags",
|
||||
help_text="Custom flags: {'is_primary': true, 'is_archived': false, etc.}"
|
||||
)
|
||||
extra_data = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
verbose_name="Extra data",
|
||||
help_text="Additional chat metadata (extensible)"
|
||||
)
|
||||
|
||||
# Новые поля для announcement и channel
|
||||
is_blocked = models.BooleanField(
|
||||
default=False,
|
||||
@ -195,6 +235,8 @@ class Chat(models.Model):
|
||||
models.Index(fields=["type", "is_main"]),
|
||||
models.Index(fields=["department"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
# NEW: Index for GenericForeignKey
|
||||
models.Index(fields=["context_content_type", "context_object_id"], name="chat_context_idx"),
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@ -259,37 +301,58 @@ class Chat(models.Model):
|
||||
)
|
||||
return rs.unread_count if rs else 0
|
||||
|
||||
@property
|
||||
def get_participants(self):
|
||||
"""
|
||||
Возвращает QuerySet участников чата (по типу).
|
||||
Для отдела берём активные связи + руководителя.
|
||||
Возвращает QuerySet участников чата.
|
||||
|
||||
Использует callback из settings.COMMUNICATIONS_PARTICIPANT_RESOLVER
|
||||
для проектно-специфичной логики (EUSRR: Department, EmployeeDepartment).
|
||||
|
||||
Fallback логика:
|
||||
- private: M2M participants
|
||||
- global: все активные пользователи
|
||||
- announcement/channel с include_all_users: все активные
|
||||
- иначе: ChatMembership + participants
|
||||
"""
|
||||
# Попытка использовать callback из settings
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
resolver_path = getattr(settings, 'COMMUNICATIONS_PARTICIPANT_RESOLVER', None)
|
||||
|
||||
if resolver_path:
|
||||
try:
|
||||
resolver_func = import_string(resolver_path)
|
||||
result = resolver_func(self)
|
||||
if result is not None:
|
||||
return result
|
||||
except (ImportError, AttributeError) as e:
|
||||
# Логируем ошибку, но не падаем
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to import participant resolver '{resolver_path}': {e}")
|
||||
|
||||
# Fallback логика (универсальная, без бизнес-специфики)
|
||||
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)
|
||||
if self.type in ["announcement", "channel"]:
|
||||
# Если include_all_employees - все активные сотрудники
|
||||
if self.include_all_employees:
|
||||
return Employee.objects.filter(is_active=True)
|
||||
# Иначе только явно добавленные через participants или ChatMembership
|
||||
from .models import ChatMembership
|
||||
membership_ids = ChatMembership.objects.filter(
|
||||
chat=self
|
||||
).values_list("user_id", flat=True)
|
||||
return Employee.objects.filter(
|
||||
Q(id__in=self.participants.values_list('id', flat=True)) |
|
||||
Q(id__in=membership_ids)
|
||||
).distinct()
|
||||
return Employee.objects.none()
|
||||
|
||||
if self.type in ["announcement", "channel"] and self.include_all_users:
|
||||
return Employee.objects.filter(is_active=True)
|
||||
|
||||
# Для group/channel/announcement без флага - явные участники
|
||||
from communications.models import ChatMembership
|
||||
|
||||
membership_ids = ChatMembership.objects.filter(
|
||||
chat=self
|
||||
).values_list("user_id", flat=True)
|
||||
|
||||
return Employee.objects.filter(
|
||||
Q(id__in=self.participants.values_list('id', flat=True)) |
|
||||
Q(id__in=membership_ids)
|
||||
).distinct()
|
||||
|
||||
def __str__(self):
|
||||
if self.type == "private":
|
||||
|
||||
@ -200,3 +200,103 @@ def _build_links_for_dept(dept: Department, serializer) -> list[dict]:
|
||||
links.insert(0, {"employee": head_data, "role": None, "is_active": True})
|
||||
|
||||
return links
|
||||
|
||||
|
||||
# ===== Communications callbacks (EUSRR-specific) =====
|
||||
|
||||
def resolve_chat_participants_for_department(context_object, **kwargs):
|
||||
"""
|
||||
EUSRR callback для разрешения участников чата отдела.
|
||||
|
||||
Args:
|
||||
context_object: Department instance
|
||||
**kwargs: Дополнительные параметры (chat, type и т.д.)
|
||||
|
||||
Returns:
|
||||
QuerySet Employee objects - участники чата
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
# Проверяем что это Department
|
||||
if not isinstance(context_object, Department):
|
||||
return Employee.objects.none()
|
||||
|
||||
department = context_object
|
||||
|
||||
# Получаем активных сотрудников отдела
|
||||
employee_ids = EmployeeDepartment.objects.filter(
|
||||
department_id=department.id,
|
||||
is_active=True
|
||||
).values_list("employee_id", flat=True)
|
||||
|
||||
# Включаем руководителя отдела
|
||||
return Employee.objects.filter(
|
||||
Q(id__in=employee_ids) | Q(id=department.head_id)
|
||||
).distinct()
|
||||
|
||||
|
||||
def resolve_chat_participants(chat):
|
||||
"""
|
||||
Главная функция для разрешения участников чата в EUSRR.
|
||||
|
||||
Вызывается из Chat.get_participants() через настройки.
|
||||
|
||||
Args:
|
||||
chat: Chat instance
|
||||
|
||||
Returns:
|
||||
QuerySet Employee objects или None (если не обработали)
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
# 1. Приватные чаты - используем M2M participants
|
||||
if chat.type == "private":
|
||||
return chat.participants.all()
|
||||
|
||||
# 2. Глобальные чаты - все активные пользователи
|
||||
if chat.type == "global":
|
||||
return Employee.objects.filter(is_active=True)
|
||||
|
||||
# 3. Announcement / Channel с include_all_users
|
||||
if chat.type in ["announcement", "channel"] and chat.include_all_users:
|
||||
return Employee.objects.filter(is_active=True)
|
||||
|
||||
# 4. Context-based чаты
|
||||
if chat.context_object:
|
||||
# Department context
|
||||
if isinstance(chat.context_object, Department):
|
||||
return resolve_chat_participants_for_department(
|
||||
chat.context_object,
|
||||
chat=chat,
|
||||
type=chat.type
|
||||
)
|
||||
|
||||
# Можно добавить другие типы контекстов:
|
||||
# - Project
|
||||
# - Team
|
||||
# - Event
|
||||
# и т.д.
|
||||
|
||||
# 5. Legacy: department field (DEPRECATED, для обратной совместимости)
|
||||
if chat.type == "department" and hasattr(chat, 'department') and chat.department_id:
|
||||
employee_ids = EmployeeDepartment.objects.filter(
|
||||
department_id=chat.department_id,
|
||||
is_active=True
|
||||
).values_list("employee_id", flat=True)
|
||||
|
||||
return Employee.objects.filter(
|
||||
Q(id__in=employee_ids) | Q(id=chat.department.head_id)
|
||||
).distinct()
|
||||
|
||||
# 6. Fallback: ChatMembership или participants
|
||||
from communications.models import ChatMembership
|
||||
|
||||
membership_ids = ChatMembership.objects.filter(
|
||||
chat=chat
|
||||
).values_list("user_id", flat=True)
|
||||
|
||||
participant_ids = chat.participants.values_list('id', flat=True)
|
||||
|
||||
return Employee.objects.filter(
|
||||
Q(id__in=participant_ids) | Q(id__in=membership_ids)
|
||||
).distinct()
|
||||
|
||||
@ -609,3 +609,8 @@ CORS_ALLOWED_ORIGINS = [
|
||||
"http://127.0.0.1:3000",
|
||||
]
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# ===== Communications App Settings =====
|
||||
# Функция для разрешения участников чата (callback)
|
||||
# Позволяет подключить бизнес-специфичную логику (EUSRR: Department, EmployeeDepartment)
|
||||
COMMUNICATIONS_PARTICIPANT_RESOLVER = 'employees.utils.resolve_chat_participants'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user