fix(requests): исправлена отправка уведомлений при создании заявления
ПРОБЛЕМА: Уведомления не отправлялись при создании нового заявления
ПРИЧИНА: Сигнал post_save срабатывал ДО установки ManyToMany связей (recipients, cc_users)
РЕШЕНИЕ:
- Добавлен сигнал m2m_changed для отслеживания установки recipients/cc_users
- При создании заявления оно помечается как ожидающее отправки уведомлений
- После установки recipients через .set() срабатывает m2m_changed и отправляет уведомления
- Используется глобальный set _pending_new_requests для отслеживания
УЛУЧШЕНИЯ:
- Добавлено подробное логирование процесса создания и отправки уведомлений
- Разные эмодзи-иконки для разных типов получателей (� адресовано, � в копии, ✅ на согласование)
- Все типы используют notification_type='request_new' (существующий в БД)
Файлы:
- notification_signals.py: добавлены сигналы m2m_changed
- serializers.py: добавлено логирование создания и установки recipients
This commit is contained in:
parent
f0509a0ba7
commit
a417ddfb8f
@ -363,6 +363,9 @@ class RequestWriteSerializer(serializers.ModelSerializer):
|
||||
cc_user_ids = validated_data.pop('cc_user_ids', [])
|
||||
departments = validated_data.pop('departments', [])
|
||||
|
||||
# Отладка: проверяем что передается
|
||||
print(f"📝 [SERIALIZER] create: recipient_ids={recipient_ids}, cc_user_ids={cc_user_ids}")
|
||||
|
||||
if not self._is_power():
|
||||
# обычный пользователь: запрещённые поля убираем/переписываем
|
||||
validated_data.pop("status", None)
|
||||
@ -379,6 +382,7 @@ class RequestWriteSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Создаем заявку
|
||||
request_obj = super().create(validated_data)
|
||||
print(f"✅ [SERIALIZER] Заявление #{request_obj.id} создано, устанавливаем recipients...")
|
||||
|
||||
# Устанавливаем связи ManyToMany
|
||||
if departments:
|
||||
@ -387,6 +391,11 @@ class RequestWriteSerializer(serializers.ModelSerializer):
|
||||
self._set_recipients(request_obj, recipient_ids, is_cc=False)
|
||||
self._set_recipients(request_obj, cc_user_ids, is_cc=True)
|
||||
|
||||
# Проверяем что сохранилось
|
||||
recipients_count = request_obj.recipients.count()
|
||||
cc_count = request_obj.cc_users.count()
|
||||
print(f"✅ [SERIALIZER] Recipients установлены: {recipients_count} основных, {cc_count} в копии")
|
||||
|
||||
return request_obj
|
||||
|
||||
def update(self, instance, validated_data: dict[str, Any]) -> Request:
|
||||
|
||||
@ -10,7 +10,8 @@ Signals для автоматической генерации уведомле
|
||||
"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_save, pre_save, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from notifications.services import NotificationService
|
||||
|
||||
@ -19,30 +20,33 @@ from .models import Request, RequestComment
|
||||
Employee = get_user_model()
|
||||
|
||||
|
||||
# Флаг для отслеживания новых заявлений, ожидающих уведомления
|
||||
_pending_new_requests = set()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Request)
|
||||
def create_request_notifications(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Создает уведомления при создании или изменении заявления.
|
||||
|
||||
Обрабатывает:
|
||||
1. Новое заявление - уведомление руководителю/ответственным
|
||||
1. Новое заявление - помечаем для отправки уведомлений после установки recipients
|
||||
2. Изменение статуса - уведомление автору
|
||||
"""
|
||||
request_obj = instance
|
||||
|
||||
# Логирование для отладки
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
f"[SIGNAL] create_request_notifications: created={created}, "
|
||||
f"status={request_obj.status}, id={request_obj.id}"
|
||||
)
|
||||
print(f"🔔 [SIGNAL] create_request_notifications: created={created}, status={request_obj.status}, id={request_obj.id}")
|
||||
|
||||
if created and request_obj.status != "draft":
|
||||
# Новая заявка - уведомляем ответственных
|
||||
logger.info(f"[SIGNAL] Вызываем notify_new_request для заявления #{request_obj.id}")
|
||||
notify_new_request(request_obj)
|
||||
elif not created:
|
||||
# Помечаем заявление как ожидающее отправки уведомлений
|
||||
# Уведомления будут отправлены после установки recipients через m2m_changed
|
||||
print(f"📌 [SIGNAL] Помечаем заявление #{request_obj.id} для отправки уведомлений после установки recipients")
|
||||
_pending_new_requests.add(request_obj.id)
|
||||
else:
|
||||
print(f"⏭️ [SIGNAL] Пропускаем: created={created}, status={request_obj.status}")
|
||||
|
||||
if not created:
|
||||
# Проверяем изменение статуса через сохраненный атрибут _old_status
|
||||
if hasattr(request_obj, "_old_status"):
|
||||
old_status = request_obj._old_status
|
||||
@ -66,6 +70,37 @@ def track_status_change(sender, instance, **kwargs):
|
||||
instance._old_status = None
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Request.recipients.through)
|
||||
def notify_on_recipients_changed(sender, instance, action, **kwargs):
|
||||
"""
|
||||
Отправляет уведомления о новом заявлении ПОСЛЕ установки recipients.
|
||||
|
||||
Срабатывает когда recipients устанавливаются через .set(), .add() и т.д.
|
||||
"""
|
||||
# Проверяем что это завершение операции установки recipients
|
||||
if action == "post_add" and instance.id in _pending_new_requests:
|
||||
print(f"🎯 [M2M_SIGNAL] Recipients установлены для заявления #{instance.id}, отправляем уведомления")
|
||||
_pending_new_requests.discard(instance.id)
|
||||
notify_new_request(instance)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Request.cc_users.through)
|
||||
def notify_on_cc_users_changed(sender, instance, action, **kwargs):
|
||||
"""
|
||||
Дополнительная проверка для cc_users.
|
||||
|
||||
Если recipients не были установлены, но установлены cc_users,
|
||||
также отправляем уведомления.
|
||||
"""
|
||||
# Если это завершение добавления cc_users И заявление все еще ожидает уведомлений
|
||||
if action == "post_add" and instance.id in _pending_new_requests:
|
||||
# Проверяем что recipients пусты (значит уведомления еще не отправлены)
|
||||
if instance.recipients.count() == 0:
|
||||
print(f"🎯 [M2M_SIGNAL] CC users установлены для заявления #{instance.id} (без recipients), отправляем уведомления")
|
||||
_pending_new_requests.discard(instance.id)
|
||||
notify_new_request(instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=RequestComment)
|
||||
def create_comment_notification(sender, instance, created, **kwargs):
|
||||
"""
|
||||
@ -120,8 +155,10 @@ def create_comment_notification(sender, instance, created, **kwargs):
|
||||
# Отправляем уведомления
|
||||
author_name = author.get_full_name() or author.username
|
||||
request_type = request_obj.get_type_display()
|
||||
employee_name = request_obj.employee.get_full_name() or request_obj.employee.username
|
||||
|
||||
employee_name = (
|
||||
request_obj.employee.get_full_name() or request_obj.employee.username
|
||||
)
|
||||
|
||||
for recipient in recipients_set:
|
||||
NotificationService.create_notification(
|
||||
recipient=recipient,
|
||||
@ -156,20 +193,23 @@ def notify_new_request(request_obj):
|
||||
|
||||
При sent_to_all_department=True отправляет всем сотрудникам отделов
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
print(f"📨 [notify_new_request] Начинаем обработку заявления #{request_obj.id}")
|
||||
|
||||
recipients_set = set()
|
||||
|
||||
# 1. Основные получатели
|
||||
recipients_count = request_obj.recipients.count()
|
||||
print(f" Recipients в БД: {recipients_count}")
|
||||
for recipient in request_obj.recipients.filter(is_active=True):
|
||||
recipients_set.add(recipient)
|
||||
logger.info(f"[notify_new_request] Добавлен основной получатель: {recipient.username}")
|
||||
print(f" ✅ Добавлен основной получатель: {recipient.username} (ID: {recipient.id})")
|
||||
|
||||
# 2. Копия (CC)
|
||||
cc_count = request_obj.cc_users.count()
|
||||
print(f" CC users в БД: {cc_count}")
|
||||
for cc_user in request_obj.cc_users.filter(is_active=True):
|
||||
recipients_set.add(cc_user)
|
||||
logger.info(f"[notify_new_request] Добавлен CC: {cc_user.username}")
|
||||
print(f" ✅ Добавлен CC: {cc_user.username} (ID: {cc_user.id})")
|
||||
|
||||
# 3. Если sent_to_all_department - все сотрудники отделов
|
||||
if request_obj.sent_to_all_department:
|
||||
@ -218,17 +258,21 @@ def notify_new_request(request_obj):
|
||||
|
||||
recipients_set.update(dept_processors)
|
||||
|
||||
# Логирование итогового списка получателей
|
||||
logger.info(
|
||||
f"[notify_new_request] Всего получателей для заявления #{request_obj.id}: {len(recipients_set)}"
|
||||
)
|
||||
# Итоговое логирование
|
||||
print(f"📊 [notify_new_request] Всего получателей для заявления #{request_obj.id}: {len(recipients_set)}")
|
||||
if len(recipients_set) == 0:
|
||||
print(f"⚠️ [notify_new_request] НЕТ ПОЛУЧАТЕЛЕЙ! Уведомления не будут отправлены.")
|
||||
return
|
||||
|
||||
for r in recipients_set:
|
||||
logger.info(f" - {r.username} (ID: {r.id})")
|
||||
print(f" 👤 {r.username} (ID: {r.id})")
|
||||
|
||||
# Определяем тип уведомления для каждого получателя
|
||||
author_name = request_obj.employee.get_full_name() or request_obj.employee.username
|
||||
request_type = request_obj.get_type_display()
|
||||
comment_preview = request_obj.comment[:150] if request_obj.comment else "Без комментария"
|
||||
comment_preview = (
|
||||
request_obj.comment[:150] if request_obj.comment else "Без комментария"
|
||||
)
|
||||
|
||||
for recipient in recipients_set:
|
||||
# Определяем роль получателя
|
||||
@ -236,8 +280,8 @@ def notify_new_request(request_obj):
|
||||
is_cc = request_obj.cc_users.filter(id=recipient.id).exists()
|
||||
is_approver = request_obj.approver_id == recipient.id
|
||||
|
||||
logger.info(
|
||||
f"[notify_new_request] Отправка уведомления для {recipient.username}: "
|
||||
print(
|
||||
f" 📤 Отправка уведомления для {recipient.username}: "
|
||||
f"primary={is_primary}, cc={is_cc}, approver={is_approver}"
|
||||
)
|
||||
|
||||
@ -246,21 +290,21 @@ def notify_new_request(request_obj):
|
||||
# Основной получатель - требует действия
|
||||
title = f"📩 Вам адресовано заявление от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
notification_type = "request_new_primary"
|
||||
notification_type = "request_new" # Используем общий тип
|
||||
elif is_cc:
|
||||
# Копия - информирование
|
||||
title = f"📋 Вы в копии заявления от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
notification_type = "request_new_cc"
|
||||
notification_type = "request_new" # Используем общий тип
|
||||
elif is_approver:
|
||||
title = f"✅ Новое заявление на согласование от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
notification_type = "request_new_approver"
|
||||
notification_type = "request_new" # Используем общий тип
|
||||
else:
|
||||
# Руководитель отдела или обработчик
|
||||
title = f"📝 Новое заявление в отделе от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
notification_type = "request_new_dept"
|
||||
notification_type = "request_new" # Используем общий тип
|
||||
|
||||
NotificationService.create_notification(
|
||||
recipient=recipient,
|
||||
@ -337,13 +381,13 @@ def notify_status_change(request_obj, old_status, new_status):
|
||||
|
||||
# Формируем уведомления с подробной информацией
|
||||
request_type = request_obj.get_type_display()
|
||||
employee_name = request_obj.employee.get_full_name() or request_obj.employee.username
|
||||
approver_name = (
|
||||
request_obj.approver.get_full_name()
|
||||
if request_obj.approver
|
||||
else "Руководитель"
|
||||
employee_name = (
|
||||
request_obj.employee.get_full_name() or request_obj.employee.username
|
||||
)
|
||||
|
||||
approver_name = (
|
||||
request_obj.approver.get_full_name() if request_obj.approver else "Руководитель"
|
||||
)
|
||||
|
||||
for recipient in recipients_to_notify:
|
||||
if new_status == "approved":
|
||||
notification_type = "request_approved"
|
||||
|
||||
@ -320,19 +320,20 @@ export function initRequestCrudHandler(options) {
|
||||
}
|
||||
|
||||
await response.json();
|
||||
|
||||
|
||||
// Закрываем модальное окно
|
||||
const modal = document.getElementById('reqApproveModal');
|
||||
const modal = document.getElementById("reqApproveModal");
|
||||
if (modal) {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) bsModal.hide();
|
||||
}
|
||||
|
||||
|
||||
// Отправляем событие для обновления списка
|
||||
document.dispatchEvent(new CustomEvent('request:updated', {
|
||||
detail: { action: 'approve', id: reqId }
|
||||
}));
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("request:updated", {
|
||||
detail: { action: "approve", id: reqId },
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
alert("Не удалось одобрить заявление: " + error.message);
|
||||
}
|
||||
@ -362,19 +363,20 @@ export function initRequestCrudHandler(options) {
|
||||
}
|
||||
|
||||
await response.json();
|
||||
|
||||
|
||||
// Закрываем модальное окно
|
||||
const modal = document.getElementById('reqRejectModal');
|
||||
const modal = document.getElementById("reqRejectModal");
|
||||
if (modal) {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) bsModal.hide();
|
||||
}
|
||||
|
||||
|
||||
// Отправляем событие для обновления списка
|
||||
document.dispatchEvent(new CustomEvent('request:updated', {
|
||||
detail: { action: 'reject', id: reqId }
|
||||
}));
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("request:updated", {
|
||||
detail: { action: "reject", id: reqId },
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
alert("Не удалось отклонить заявление: " + error.message);
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
* @description Инициализация страницы списка заявлений
|
||||
*/
|
||||
|
||||
import { initRequestModalHandler } from '../components/requestModalHandler.js';
|
||||
import { initRequestCommentsHandler } from '../components/requestCommentsHandler.js';
|
||||
import { initRequestCrudHandler } from '../components/requestCrudHandler.js';
|
||||
import { initRequestListHandler } from '../components/requestListHandler.js';
|
||||
import { initRequestModalHandler } from "../components/requestModalHandler.js";
|
||||
import { initRequestCommentsHandler } from "../components/requestCommentsHandler.js";
|
||||
import { initRequestCrudHandler } from "../components/requestCrudHandler.js";
|
||||
import { initRequestListHandler } from "../components/requestListHandler.js";
|
||||
|
||||
/**
|
||||
* Инициализирует страницу списка заявлений
|
||||
@ -25,22 +25,22 @@ export function initRequestListPage(config) {
|
||||
commentsUrlTemplate,
|
||||
addCommentUrlTemplate,
|
||||
autoShowComments = false,
|
||||
useAjaxList = false
|
||||
useAjaxList = false,
|
||||
} = config;
|
||||
|
||||
// Получаем токен доступа
|
||||
const accessMeta = document.querySelector('meta[name="api-access"]');
|
||||
const ACCESS = accessMeta ? accessMeta.content : '';
|
||||
|
||||
const ACCESS = accessMeta ? accessMeta.content : "";
|
||||
|
||||
// Функция для получения CSRF токена из cookie
|
||||
function getCsrfToken() {
|
||||
const name = 'csrftoken';
|
||||
const name = "csrftoken";
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
@ -48,41 +48,41 @@ export function initRequestListPage(config) {
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
|
||||
const headers = {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
"X-CSRFToken": getCsrfToken(),
|
||||
};
|
||||
|
||||
|
||||
if (ACCESS) {
|
||||
headers['Authorization'] = 'Bearer ' + ACCESS;
|
||||
headers["Authorization"] = "Bearer " + ACCESS;
|
||||
}
|
||||
|
||||
// Если включен AJAX режим - инициализируем обработчик списка
|
||||
if (useAjaxList) {
|
||||
const userId = parseInt(document.body.dataset.userId || '0', 10);
|
||||
const canProcess = document.body.dataset.canProcess === 'true';
|
||||
|
||||
console.log('Request list AJAX mode:', {
|
||||
const userId = parseInt(document.body.dataset.userId || "0", 10);
|
||||
const canProcess = document.body.dataset.canProcess === "true";
|
||||
|
||||
console.log("Request list AJAX mode:", {
|
||||
apiListUrl,
|
||||
userId,
|
||||
canProcess
|
||||
canProcess,
|
||||
});
|
||||
|
||||
|
||||
// Инициализация списка заявлений (AJAX загрузка с бесконечной прокруткой)
|
||||
const requestListHandler = initRequestListHandler({
|
||||
apiListUrl,
|
||||
detailUrlTemplate: '/requests/{id}/',
|
||||
detailUrlTemplate: "/requests/{id}/",
|
||||
userId,
|
||||
canProcess,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
// Обработка переключения фильтров через URL параметры
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentView = urlParams.get('view') || '';
|
||||
const currentType = urlParams.get('type') || '';
|
||||
const currentStatus = urlParams.get('status') || '';
|
||||
|
||||
const currentView = urlParams.get("view") || "";
|
||||
const currentType = urlParams.get("type") || "";
|
||||
const currentStatus = urlParams.get("status") || "";
|
||||
|
||||
if (currentView && requestListHandler.setView) {
|
||||
requestListHandler.setView(currentView);
|
||||
}
|
||||
@ -96,26 +96,26 @@ export function initRequestListPage(config) {
|
||||
|
||||
// Инициализация обработчиков модальных окон
|
||||
initRequestModalHandler({
|
||||
autoShowComments
|
||||
autoShowComments,
|
||||
});
|
||||
|
||||
// Инициализация обработчика комментариев
|
||||
initRequestCommentsHandler({
|
||||
commentsUrlTemplate,
|
||||
addCommentUrlTemplate,
|
||||
autoloadCounts: true
|
||||
autoloadCounts: true,
|
||||
});
|
||||
|
||||
// Инициализация CRUD операций для заявлений
|
||||
initRequestCrudHandler({
|
||||
apiListUrl,
|
||||
apiDetailBase,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
|
||||
// Слушаем события обновления заявлений (approve/reject)
|
||||
document.addEventListener('request:updated', function(event) {
|
||||
console.log('Request updated:', event.detail);
|
||||
document.addEventListener("request:updated", function (event) {
|
||||
console.log("Request updated:", event.detail);
|
||||
// Перезагружаем страницу для обновления списка
|
||||
// TODO: в будущем можно заменить на AJAX обновление конкретной карточки
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user