fix: исправлены ошибки в системе уведомлений

- Добавлен эндпоинт удаления комментариев к заявкам (DELETE /api/v1/requests/{pk}/comments/{comment_id}/)
- Исправлены references на переменную в сигналах комментариев (comment -> instance)
- Добавлена обработка статуса cancelled для заявок в notification_type_map
- Обновлены процессоры уведомлений в task_base.py (правильные поля моделей)
- Создан скрипт генерации тестовых пользователей (scripts/create_test_users.py)
This commit is contained in:
Игорь Надеин 2026-01-09 16:55:52 +03:00
parent 92113521e5
commit 2ff0eaa538
5 changed files with 409 additions and 91 deletions

View File

@ -367,6 +367,36 @@ class RequestViewSet(viewsets.ModelViewSet):
logger.info(f"[RequestViewSet.comments] POST: comment created id={saved.id}")
return Response(ser.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["delete"], url_path="comments/(?P<comment_id>[^/.]+)")
def delete_comment(self, request: DRFRequest, pk: int | str | None = None, comment_id: int | str | None = None) -> Response:
"""Удаление комментария.
DELETE /api/v1/requests/{pk}/comments/{comment_id}/
Права: только автор комментария или staff.
"""
import logging
logger = logging.getLogger(__name__)
req_obj = self.get_object()
try:
comment = RequestComment.objects.get(id=comment_id, request=req_obj)
except RequestComment.DoesNotExist:
return Response({"detail": "Comment not found"}, status=status.HTTP_404_NOT_FOUND)
# Проверка прав: только автор или staff
if not (request.user.is_staff or comment.author == request.user):
return Response(
{"detail": "You don't have permission to delete this comment"},
status=status.HTTP_403_FORBIDDEN
)
logger.info(f"[RequestViewSet.delete_comment] user={request.user.id} deleting comment_id={comment_id}")
comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# ---------- Экшены статусов (только бизнес-валидация) ----------
@action(detail=True, methods=["post"])

View File

@ -360,31 +360,36 @@ class EventNotificationProcessor(BaseNotificationProcessor):
User = get_user_model()
event = CalendarEvent.objects.select_related('creator').get(id=event_id)
event = CalendarEvent.objects.select_related('created_by', 'employee', 'department').get(id=event_id)
# Собираем всех получателей
recipients = set()
# Участники события
for participant in event.participants.all():
recipients.add(participant)
# Если событие отдела - уведомляем всех сотрудников отдела
if event.department:
recipients.update(self.get_department_employees([event.department.id]))
# Департаменты (если есть)
if event.departments.exists():
recipients.update(self.get_department_employees(event.departments.all()))
# Если личное событие сотрудника - уведомляем только его
elif event.employee:
# Проверяем что сотрудник активен
if event.employee.is_active and event.employee.email_verified:
recipients.add(event.employee)
# Исключаем создателя и неактивных
recipients = self.get_active_employees(recipients)
if event.creator:
recipients.discard(event.creator)
# Если глобальное событие (нет department и employee) - уведомляем всех активных
else:
recipients = set(User.objects.filter(is_active=True, email_verified=True))
# Исключаем создателя
if event.created_by:
recipients.discard(event.created_by)
if not recipients:
return self.log_result(event_id, 0)
return self.log_result('event', event_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='calendar_event_created',
notification_type='event_created',
title=f'Новое событие: {event.title}',
message=truncate_text(event.description or '', 150),
content_object=event,
@ -397,10 +402,10 @@ class EventNotificationProcessor(BaseNotificationProcessor):
}
)
return self.log_result(event_id, notifications_sent)
return self.log_result('event', event_id, 'created', notifications_sent)
except Exception as exc:
return self.handle_exception(exc, f"event {event_id}")
self.handle_exception(exc)
class PostNotificationProcessor(BaseNotificationProcessor):
@ -410,55 +415,47 @@ class PostNotificationProcessor(BaseNotificationProcessor):
"""Обработка уведомлений для поста"""
try:
from feed.models import Post
from django.contrib.auth import get_user_model
post = Post.objects.select_related('author').get(id=post_id)
if post.is_deleted:
return {"status": "skipped", "reason": "deleted"}
User = get_user_model()
post = Post.objects.select_related('author', 'department').get(id=post_id)
# Собираем получателей
recipients = set()
# Департаменты
if post.target_departments.exists():
recipients.update(self.get_department_employees(post.target_departments.all()))
# Если пост для конкретного отдела - уведомляем сотрудников отдела
if post.department:
recipients.update(self.get_department_employees([post.department.id]))
else:
# Иначе уведомляем всех активных сотрудников
recipients = set(User.objects.filter(is_active=True, email_verified=True))
# Конкретные пользователи
for user in post.target_users.all():
recipients.add(user)
# Если нет конкретных получателей - всем активным
if not recipients:
from django.contrib.auth import get_user_model
User = get_user_model()
recipients = set(User.objects.filter(is_active=True))
# Исключаем автора и неактивных
recipients = self.get_active_employees(recipients)
# Исключаем автора
if post.author:
recipients.discard(post.author)
if not recipients:
return self.log_result(post_id, 0)
return self.log_result('post', post_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='feed_new_post',
title=f'Новая публикация от {post.author.get_full_name() if post.author else "Система"}',
message=truncate_text(post.content, 200),
title=f'{post.title}',
message=truncate_text(post.body, 200),
content_object=post,
action_url=f'/feed/?post={post.id}',
metadata={
'post_id': post.id,
'author_id': post.author.id if post.author else None,
'post_type': post.type,
}
)
return self.log_result(post_id, notifications_sent)
return self.log_result('post', post_id, 'created', notifications_sent)
except Exception as exc:
return self.handle_exception(exc, f"post {post_id}")
self.handle_exception(exc)
class RequestNotificationProcessor(BaseNotificationProcessor):
@ -475,76 +472,83 @@ class RequestNotificationProcessor(BaseNotificationProcessor):
try:
from requests_app.models import Request
request_obj = Request.objects.select_related('requester', 'handler').get(id=request_id)
request_obj = Request.objects.select_related('employee', 'approver').prefetch_related('recipients', 'cc_users').get(id=request_id)
# Собираем получателей в зависимости от типа уведомления
recipients = set()
if notification_type == 'created':
# Уведомляем исполнителя и копии
if request_obj.handler:
recipients.add(request_obj.handler)
# Уведомляем согласующего и получателей
if request_obj.approver:
recipients.add(request_obj.approver)
for user in request_obj.recipients.all():
recipients.add(user)
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
for user in request_obj.cc_users.all():
recipients.add(user)
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
elif notification_type == 'status_changed':
# Уведомляем создателя
if request_obj.requester:
recipients.add(request_obj.requester)
# Уведомляем создателя заявки
if request_obj.employee:
if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True):
recipients.add(request_obj.employee)
elif notification_type == 'comment_added':
# Уведомляем всех участников кроме автора комментария
if request_obj.requester:
recipients.add(request_obj.requester)
if request_obj.handler:
recipients.add(request_obj.handler)
# Уведомляем всех участников
if request_obj.employee:
if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True):
recipients.add(request_obj.employee)
if request_obj.approver:
if request_obj.approver.is_active and getattr(request_obj.approver, 'email_verified', True):
recipients.add(request_obj.approver)
for user in request_obj.recipients.all():
recipients.add(user)
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
for user in request_obj.cc_users.all():
recipients.add(user)
# Исключаем неактивных
recipients = self.get_active_employees(recipients)
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
if not recipients:
return self.log_result(request_id, 0)
return self.log_result('request', request_id, notification_type, 0)
# Формируем заголовок
title_map = {
'created': f'Новая заявка #{request_obj.id}: {request_obj.subject}',
'created': f'Новая заявка #{request_obj.id}: {request_obj.title or request_obj.get_type_display()}',
'status_changed': f'Изменён статус заявки #{request_obj.id}',
'comment_added': f'Новый комментарий к заявке #{request_obj.id}',
}
type_code_map = {
'created': 'request_created',
'created': 'request_new',
'status_changed': 'request_status_changed',
'comment_added': 'request_comment_added',
'comment_added': 'request_comment',
}
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type=type_code_map.get(notification_type, 'request_created'),
notification_type=type_code_map.get(notification_type, 'request_new'),
title=title_map.get(notification_type, f'Обновление заявки #{request_obj.id}'),
message=truncate_text(request_obj.description, 150),
message=truncate_text(request_obj.comment or '', 150),
content_object=request_obj,
action_url=f'/requests/{request_obj.id}/',
metadata={
'request_id': request_obj.id,
'subject': request_obj.subject,
'request_type': request_obj.type,
'status': request_obj.status,
}
)
return self.log_result(request_id, notifications_sent)
return self.log_result('request', request_id, notification_type, notifications_sent)
except Exception as exc:
return self.handle_exception(exc, f"request {request_id}")
self.handle_exception(exc)
class DocumentNotificationProcessor(BaseNotificationProcessor):
@ -554,35 +558,39 @@ class DocumentNotificationProcessor(BaseNotificationProcessor):
"""Обработка уведомлений для документа"""
try:
from documents.models import Document
from django.contrib.auth import get_user_model
document = Document.objects.select_related('author').get(id=document_id)
if document.is_deleted:
return {"status": "skipped", "reason": "deleted"}
User = get_user_model()
document = Document.objects.select_related('uploaded_by').prefetch_related('departments', 'recipients').get(id=document_id)
# Собираем получателей
recipients = set()
# Департаменты
if document.departments.exists():
recipients.update(self.get_department_employees(document.departments.all()))
# Если разослать всем
if document.sent_to_all:
recipients = set(User.objects.filter(is_active=True, email_verified=True))
else:
# Департаменты
if document.departments.exists():
dept_ids = [d.id for d in document.departments.all()]
recipients.update(self.get_department_employees(dept_ids))
# Конкретные получатели
for user in document.recipients.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
# Конкретные пользователи
for user in document.readers.all():
recipients.add(user)
# Исключаем автора и неактивных
recipients = self.get_active_employees(recipients)
if document.author:
recipients.discard(document.author)
# Исключаем автора
if document.uploaded_by:
recipients.discard(document.uploaded_by)
if not recipients:
return self.log_result(document_id, 0)
return self.log_result('document', document_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='document_created',
notification_type='document_ready',
title=f'Новый документ: {document.title}',
message=truncate_text(document.description or '', 150),
content_object=document,
@ -590,11 +598,10 @@ class DocumentNotificationProcessor(BaseNotificationProcessor):
metadata={
'document_id': document.id,
'document_title': document.title,
'document_type': document.document_type,
}
)
return self.log_result(document_id, notifications_sent)
return self.log_result('document', document_id, 'created', notifications_sent)
except Exception as exc:
return self.handle_exception(exc, f"document {document_id}")
self.handle_exception(exc)

View File

@ -356,6 +356,8 @@ def process_request_notifications_task(
'created': 'created',
'approved': 'status_changed',
'rejected': 'status_changed',
'cancelled': 'status_changed',
'updated': 'status_changed',
'comment': 'comment_added',
}
return processor.process(request_id, notification_type_map.get(action, 'created'))

View File

@ -67,7 +67,8 @@ def create_request_notifications(sender, instance, created, **kwargs):
# Определяем action по статусу
action = 'approved' if new_status == 'approved' else \
'rejected' if new_status == 'rejected' else 'updated'
'rejected' if new_status == 'rejected' else \
'cancelled' if new_status == 'cancelled' else 'updated'
def send_task():
"""Отложенная отправка задачи после commit транзакции"""
@ -215,8 +216,9 @@ def create_comment_notification(sender, instance, created, **kwargs):
notify_request_comment(instance)
transaction.on_commit(send_task)
author = comment.author
author = instance.author
recipients_set = set()
request_obj = instance.request
# Автор заявки
if request_obj.employee.id != author.id:
@ -264,14 +266,14 @@ def create_comment_notification(sender, instance, created, **kwargs):
title=f"💬 Новый комментарий к заявлению от {employee_name}",
message=(
f"{author_name} прокомментировал заявление "
f'"{request_type}": {comment.text[:100]}'
f'"{request_type}": {instance.text[:100]}'
),
content_object=request_obj,
action_url=f"/requests/{request_obj.id}/",
metadata={
"request_id": request_obj.id,
"request_type": request_obj.type,
"comment_id": comment.id,
"comment_id": instance.id,
"author_id": author.id,
},
)

View File

@ -0,0 +1,277 @@
"""
Скрипт для создания тестовых пользователей в корпоративном портале.
Создает 100+ пользователей с разными ролями и отделами.
"""
import os
import sys
from pathlib import Path
# Добавляем путь к backend в sys.path
backend_dir = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(backend_dir))
import django
from datetime import date, timedelta
import random
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from employees.models import Department, Position, EmployeeDepartment
User = get_user_model()
# Списки для генерации данных
FIRST_NAMES = [
'Александр', 'Дмитрий', 'Максим', 'Сергей', 'Андрей', 'Алексей', 'Артем', 'Илья',
'Кирилл', 'Михаил', 'Иван', 'Даниил', 'Егор', 'Никита', 'Матвей', 'Роман',
'Владислав', 'Ярослав', 'Тимофей', 'Владимир', 'Павел', 'Николай', 'Денис', 'Олег',
'Анна', 'Мария', 'Елена', 'Ольга', 'Татьяна', 'Наталья', 'Екатерина', 'Ирина',
'Юлия', 'Светлана', 'Людмила', 'Галина', 'Анастасия', 'Дарья', 'Виктория', 'Валерия',
'Ксения', 'Полина', 'Алина', 'Вероника', 'София', 'Марина', 'Оксана', 'Алла'
]
LAST_NAMES = [
'Иванов', 'Петров', 'Сидоров', 'Смирнов', 'Кузнецов', 'Попов', 'Васильев', 'Соколов',
'Михайлов', 'Новиков', 'Федоров', 'Морозов', 'Волков', 'Алексеев', 'Лебедев', 'Семенов',
'Егоров', 'Павлов', 'Козлов', 'Степанов', 'Николаев', 'Орлов', 'Андреев', 'Макаров',
'Никитин', 'Захаров', 'Зайцев', 'Соловьев', 'Борисов', 'Яковлев', 'Григорьев', 'Романов',
'Воробьев', 'Сергеев', 'Фролов', 'Дмитриев', 'Ковалев', 'Белов', 'Комаров', 'Жуков',
'Иванова', 'Петрова', 'Сидорова', 'Смирнова', 'Кузнецова', 'Попова', 'Васильева', 'Соколова'
]
PATRONYMICS = [
'Александрович', 'Дмитриевич', 'Максимович', 'Сергеевич', 'Андреевич', 'Алексеевич',
'Артемович', 'Ильич', 'Кириллович', 'Михайлович', 'Иванович', 'Даниилович',
'Александровна', 'Дмитриевна', 'Максимовна', 'Сергеевна', 'Андреевна', 'Алексеевна',
'Артемовна', 'Ильинична', 'Кирилловна', 'Михайловна', 'Ивановна', 'Данииловна'
]
DEPARTMENTS_DATA = [
{'name': 'Разработка', 'code': 'DEV'},
{'name': 'Тестирование', 'code': 'QA'},
{'name': 'Продажи', 'code': 'SALES'},
{'name': 'Маркетинг', 'code': 'MKT'},
{'name': 'HR', 'code': 'HR'},
{'name': 'Финансы', 'code': 'FIN'},
{'name': 'Бухгалтерия', 'code': 'ACC'},
{'name': 'Логистика', 'code': 'LOG'},
{'name': 'Производство', 'code': 'PROD'},
{'name': 'Закупки', 'code': 'PURCH'},
]
POSITIONS_DATA = [
'Младший специалист',
'Специалист',
'Старший специалист',
'Ведущий специалист',
'Менеджер',
'Старший менеджер',
'Руководитель группы',
'Руководитель отдела',
'Директор',
'Стажер',
]
def create_departments():
"""Создание отделов"""
print('\n📁 Создание отделов...')
departments = []
for dept_data in DEPARTMENTS_DATA:
dept, created = Department.objects.get_or_create(
name=dept_data['name'],
defaults={'description': f'Отдел {dept_data["name"]}'}
)
departments.append(dept)
if created:
print(f' ✅ Создан отдел: {dept.name}')
else:
print(f' ℹ️ Отдел существует: {dept.name}')
return departments
def create_positions():
"""Создание должностей"""
print('\n💼 Создание должностей...')
positions = []
for pos_name in POSITIONS_DATA:
pos, created = Position.objects.get_or_create(
name=pos_name
)
positions.append(pos)
if created:
print(f' ✅ Создана должность: {pos.name}')
return positions
def generate_email(first_name, last_name, counter):
"""Генерация email"""
first = first_name.lower()
last = last_name.lower()
# Транслитерация
translit = {
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e',
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '',
'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'
}
first_en = ''.join(translit.get(c, c) for c in first)
last_en = ''.join(translit.get(c, c) for c in last)
return f'{first_en}.{last_en}{counter}@company.local'
def generate_phone():
"""Генерация номера телефона"""
return f'+7{random.randint(900, 999)}{random.randint(1000000, 9999999)}'
def generate_birthdate():
"""Генерация даты рождения (25-65 лет)"""
years_ago = random.randint(25, 65)
days_offset = random.randint(0, 364)
return date.today() - timedelta(days=years_ago * 365 + days_offset)
def create_users(count=120, departments=None, positions=None):
"""Создание пользователей"""
print(f'\n👥 Создание {count} пользователей...')
if not departments:
departments = list(Department.objects.all())
if not positions:
positions = list(Position.objects.all())
created_count = 0
skipped_count = 0
for i in range(1, count + 1):
first_name = random.choice(FIRST_NAMES)
last_name = random.choice(LAST_NAMES)
patronymic = random.choice(PATRONYMICS)
email = generate_email(first_name, last_name, i)
# Проверяем, существует ли пользователь
if User.objects.filter(email=email).exists():
skipped_count += 1
continue
try:
# Создаем пользователя
user = User.objects.create_user(
email=email,
password='test123', # Простой пароль для тестирования
first_name=first_name,
last_name=last_name,
patronymic=patronymic,
phone_number=generate_phone(),
birth_date=generate_birthdate(),
is_active=True,
email_verified=True,
is_staff=random.random() < 0.1, # 10% - staff
send_activation_email=False
)
# Назначаем должность
position = random.choice(positions)
user.position = position
user.save()
# Назначаем отдел(ы)
# 70% - один отдел, 25% - два отдела, 5% - три отдела
dept_count = random.choices([1, 2, 3], weights=[70, 25, 5])[0]
user_departments = random.sample(departments, min(dept_count, len(departments)))
for dept in user_departments:
EmployeeDepartment.objects.create(
employee=user,
department=dept,
is_active=True
)
created_count += 1
if created_count % 10 == 0:
print(f' ✅ Создано {created_count} пользователей...')
except Exception as e:
print(f' ❌ Ошибка создания пользователя {email}: {e}')
skipped_count += 1
return created_count, skipped_count
def print_statistics():
"""Вывод статистики"""
print('\n' + '=' * 60)
print('📊 СТАТИСТИКА БАЗЫ ДАННЫХ')
print('=' * 60)
total_users = User.objects.count()
active_users = User.objects.filter(is_active=True).count()
staff_users = User.objects.filter(is_staff=True).count()
print(f'\n👥 Пользователи:')
print(f' Всего: {total_users}')
print(f' Активных: {active_users}')
print(f' Персонал: {staff_users}')
departments = Department.objects.all()
print(f'\n📁 Отделы: {departments.count()}')
for dept in departments:
emp_count = EmployeeDepartment.objects.filter(
department=dept,
is_active=True
).count()
print(f' • {dept.name}: {emp_count} сотрудников')
positions = Position.objects.all()
print(f'\n💼 Должности: {positions.count()}')
for pos in positions:
emp_count = User.objects.filter(position=pos).count()
if emp_count > 0:
print(f' • {pos.name}: {emp_count} сотрудников')
print('\n' + '=' * 60)
def main():
print('=' * 60)
print('🚀 ГЕНЕРАЦИЯ ТЕСТОВЫХ ПОЛЬЗОВАТЕЛЕЙ')
print('=' * 60)
# Создаем отделы и должности
departments = create_departments()
positions = create_positions()
# Создаем пользователей
created, skipped = create_users(
count=120,
departments=departments,
positions=positions
)
print(f'\n✅ Создание завершено!')
print(f' Создано: {created} пользователей')
if skipped > 0:
print(f' Пропущено: {skipped} (уже существуют)')
# Выводим статистику
print_statistics()
print('\n💡 Данные для входа:')
print(' Username: user001, user002, ... user120')
print(' Password: test123')
print(' Email: имя.фамилия1@company.local, и т.д.')
print('\n' + '=' * 60)
if __name__ == '__main__':
main()