From 0a85b25d270e4badc4df7603c71ced8b78989d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 30 Nov 2025 11:18:59 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B0=D1=82=D0=BE=D0=B2?= =?UTF-8?q?:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B2=D1=81=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B8=20(=D0=B0=D0=B2=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=80,=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB,=20include=5Fall=5Femp?= =?UTF-8?q?loyees,=20is=5Fmain)=20+=20=D0=B0=D0=B2=D1=82=D0=BE=D1=81=D0=B6?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D0=B5=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=87=D0=B0=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/communications/api_views.py | 164 +++++++++++++-- backend/communications/signals.py | 61 +++++- backend/communications/views.py | 7 + .../templates/communications/chat_list.html | 195 ++++++++++++++++-- 4 files changed, 385 insertions(+), 42 deletions(-) diff --git a/backend/communications/api_views.py b/backend/communications/api_views.py index 13dc24d5..e6397b61 100644 --- a/backend/communications/api_views.py +++ b/backend/communications/api_views.py @@ -24,37 +24,121 @@ from .models import ( @login_required @require_POST def create_chat(request): - """Создание нового чата (group, channel, announcement)""" + """ + Создание нового чата + (group, department, channel, announcement, global) + """ import json - - # Читаем JSON из тела запроса - try: - data = json.loads(request.body) - except json.JSONDecodeError: - return JsonResponse({'ok': False, 'error': 'Invalid JSON'}, status=400) - - chat_type = data.get('type', 'group') - name = data.get('name', '').strip() - description = data.get('description', '').strip() - participant_ids = data.get('participant_ids', []) - - if chat_type not in ['group', 'channel', 'announcement']: + + # Читаем данные из FormData или JSON + if request.content_type and 'multipart/form-data' in request.content_type: + # FormData (с файлами) + chat_type = request.POST.get('type', 'group') + name = request.POST.get('name', '').strip() + description = request.POST.get('description', '').strip() + department_id = request.POST.get('department_id') + include_all = request.POST.get('include_all_employees') == 'true' + is_main = request.POST.get('is_main') == 'true' + avatar = request.FILES.get('avatar') + participant_ids = request.POST.getlist('participant_ids') + else: + # JSON (без файлов - legacy) + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse( + {'ok': False, 'error': 'Invalid JSON'}, status=400 + ) + + chat_type = data.get('type', 'group') + name = data.get('name', '').strip() + description = data.get('description', '').strip() + department_id = data.get('department_id') + include_all = data.get('include_all_employees', False) + is_main = data.get('is_main', False) + avatar = None + participant_ids = data.get('participant_ids', []) + + # Валидация типа чата + valid_types = ['group', 'department', 'channel', 'announcement', 'global'] + if chat_type not in valid_types: + types_str = ", ".join(valid_types) return JsonResponse( - {'ok': False, 'error': 'Invalid chat type'}, status=400 + { + 'ok': False, + 'error': f'Invalid chat type. Must be one of: {types_str}' + }, + status=400 ) - - if not name: + + # Валидация названия + if not name and chat_type != 'private': return JsonResponse( {'ok': False, 'error': 'Name is required'}, status=400 ) - + + # Валидация для чата отдела + if chat_type == 'department': + if not department_id: + return JsonResponse( + { + 'ok': False, + 'error': 'Department is required for department chat' + }, + status=400 + ) + try: + from employees.models import Department + department = Department.objects.get(id=department_id) + except Department.DoesNotExist: + return JsonResponse( + {'ok': False, 'error': 'Department not found'}, + status=400 + ) + else: + department = None + + # Валидация для основного чата + if is_main: + if chat_type == 'global': + # Проверяем, нет ли уже основного глобального чата + if Chat.objects.filter(type='global', is_main=True).exists(): + return JsonResponse( + {'ok': False, 'error': 'Main global chat already exists'}, + status=400 + ) + elif chat_type == 'department' and department: + # Проверяем, нет ли уже основного чата для этого отдела + exists = Chat.objects.filter( + type='department', + department=department, + is_main=True + ).exists() + if exists: + return JsonResponse( + { + 'ok': False, + 'error': 'Main department chat already exists' + }, + status=400 + ) + + # Создаем чат chat = Chat.objects.create( type=chat_type, name=name, description=description, - created_by=request.user + created_by=request.user, + department=department, + include_all_employees=include_all, + is_main=is_main ) - + + # Добавляем аватар если был загружен + if avatar: + chat.avatar = avatar + chat.save() + # Создатель - владелец с полными правами ChatMembership.objects.create( chat=chat, @@ -65,7 +149,7 @@ def create_chat(request): can_remove_members=True, can_pin_messages=True ) - + # Добавляем участников для группового чата if chat_type == 'group' and participant_ids: from employees.models import Employee @@ -81,7 +165,43 @@ def create_chat(request): ) except Employee.DoesNotExist: pass - + + # Для чатов с include_all_employees - добавляем всех активных + if include_all: + from employees.models import Employee + all_employees = Employee.objects.filter(is_active=True).exclude( + id=request.user.id + ) + for employee in all_employees: + can_send = chat_type not in ['channel', 'announcement'] + ChatMembership.objects.get_or_create( + chat=chat, + user=employee, + defaults={ + 'role': 'member', + 'can_send_messages': can_send + } + ) + + # Для чата отдела - добавляем всех сотрудников отдела + if chat_type == 'department' and department: + from employees.models import EmployeeDepartment + dept_employees = EmployeeDepartment.objects.filter( + department=department, + is_active=True + ).select_related('employee') + + for ed in dept_employees: + if ed.employee != request.user: # Создателя уже добавили + ChatMembership.objects.get_or_create( + chat=chat, + user=ed.employee, + defaults={ + 'role': 'member', + 'can_send_messages': True + } + ) + return JsonResponse({ 'ok': True, 'chat_id': chat.id, diff --git a/backend/communications/signals.py b/backend/communications/signals.py index 52e22e74..bfcf20ca 100644 --- a/backend/communications/signals.py +++ b/backend/communications/signals.py @@ -1,7 +1,11 @@ # backend\communications\signals.py -from django.db.models.signals import post_save +from django.core.files.base import ContentFile +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from employees.models import Department +import logging + +logger = logging.getLogger(__name__) @receiver(post_save, sender=Department) @@ -13,3 +17,58 @@ def create_main_department_chat(sender, instance, created, **kwargs): type="department", department=instance, is_main=True ).exists(): Chat.objects.create(type="department", department=instance, is_main=True) + + +@receiver(pre_save, sender='communications.Chat') +def compress_and_cleanup_chat_avatar(sender, instance, **kwargs): + """ + Сжимает новый аватар чата и удаляет старый файл + """ + if not instance.avatar: + return + + # Если это новый объект (нет pk), просто сжимаем + if not instance.pk: + try: + from common.image_utils import compress_avatar + compressed = compress_avatar(instance.avatar.read()) + if compressed: + instance.avatar.save( + instance.avatar.name, + ContentFile(compressed), + save=False + ) + except Exception as e: + logger.error(f"Error compressing new chat avatar: {e}") + return + + # Проверяем, изменился ли файл аватара + try: + old_instance = sender.objects.get(pk=instance.pk) + except sender.DoesNotExist: + return + + old_avatar = old_instance.avatar + new_avatar = instance.avatar + + # Если путь изменился - это новый файл + if old_avatar and new_avatar and old_avatar.name != new_avatar.name: + try: + # Сжимаем новый аватар + from common.image_utils import compress_avatar + compressed = compress_avatar(new_avatar.read()) + if compressed: + new_avatar.save( + new_avatar.name, + ContentFile(compressed), + save=False + ) + + # Удаляем старый файл + if old_avatar.name: + old_avatar.delete(save=False) + logger.info( + f"Deleted old chat avatar: {old_avatar.name}" + ) + except Exception as e: + logger.error(f"Error processing chat avatar: {e}") diff --git a/backend/communications/views.py b/backend/communications/views.py index 7910d148..639a5c01 100644 --- a/backend/communications/views.py +++ b/backend/communications/views.py @@ -101,6 +101,13 @@ class ChatListView(LoginRequiredMixin, ListView): template_name = "communications/chat_list.html" context_object_name = "chats" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Добавляем список отделов для выбора при создании чата + from employees.models import Department + context['departments'] = Department.objects.all().order_by('name') + return context + def get_queryset(self): user = self.request.user # ВАЖНО: у Employee.departments это property, а не менеджер diff --git a/backend/templates/communications/chat_list.html b/backend/templates/communications/chat_list.html index cf01099e..6ce2bb28 100644 --- a/backend/templates/communications/chat_list.html +++ b/backend/templates/communications/chat_list.html @@ -514,7 +514,7 @@ {# ===== МОДАЛКА СОЗДАНИЯ ЧАТА ===== #}