feat: add comprehensive logging for notification system tracking
This commit is contained in:
parent
6cebd772f2
commit
80b19dd1b3
@ -62,11 +62,21 @@ class EmailNotificationSender:
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[EmailNotificationSender.send_notification_email] НАЧАЛО: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient_email}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Получаем категорию
|
||||
category_code = notification.notification_type.category.code
|
||||
category_name = notification.notification_type.category.name
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] Категория: {category_name} ({category_code})"
|
||||
)
|
||||
|
||||
# Формируем тему письма
|
||||
if custom_subject:
|
||||
subject = custom_subject
|
||||
@ -74,6 +84,10 @@ class EmailNotificationSender:
|
||||
icon = cls.CATEGORY_ICONS.get(category_code, '🔔')
|
||||
subject = f"{icon} {notification.title}"
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] Тема письма: '{subject}'"
|
||||
)
|
||||
|
||||
# Формируем контекст для шаблона
|
||||
context = {
|
||||
'notification': notification,
|
||||
@ -86,6 +100,12 @@ class EmailNotificationSender:
|
||||
'site_url': cls._get_site_url(),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] ➡️ Вызов send_templated_mail: "
|
||||
f"template='notifications/email/notification', "
|
||||
f"to={recipient_email}"
|
||||
)
|
||||
|
||||
# Отправляем email
|
||||
send_templated_mail(
|
||||
subject=subject,
|
||||
@ -95,13 +115,17 @@ class EmailNotificationSender:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Email уведомление отправлено: {notification.id} -> {recipient_email}"
|
||||
f"[EmailNotificationSender] ✅ Email УСПЕШНО отправлен: "
|
||||
f"notification_id={notification.id} -> {recipient_email}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка отправки email уведомления {notification.id}: {e}",
|
||||
f"[EmailNotificationSender] ❌ ОШИБКА отправки email: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient_email}, "
|
||||
f"error={type(e).__name__}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
@ -54,8 +54,11 @@ class NotificationService:
|
||||
Созданное уведомление или None если тип не найден
|
||||
"""
|
||||
logger.info(
|
||||
f"[create_notification] Starting for recipient={recipient.id} "
|
||||
f"type={notification_type_code} send_immediately={send_immediately}"
|
||||
f"[NotificationService.create_notification] НАЧАЛО: "
|
||||
f"recipient={recipient.id} ({recipient.email}), "
|
||||
f"type={notification_type_code}, "
|
||||
f"send_immediately={send_immediately}, "
|
||||
f"title='{title[:50]}...'"
|
||||
)
|
||||
|
||||
try:
|
||||
@ -63,9 +66,14 @@ class NotificationService:
|
||||
code=notification_type_code,
|
||||
is_active=True
|
||||
)
|
||||
logger.info(
|
||||
f"[NotificationService.create_notification] Тип уведомления найден: "
|
||||
f"{notification_type.name} (category={notification_type.category.code})"
|
||||
)
|
||||
except NotificationType.DoesNotExist:
|
||||
logger.warning(
|
||||
f"[create_notification] Type {notification_type_code} not found"
|
||||
logger.error(
|
||||
f"[NotificationService.create_notification] ❌ ОШИБКА: "
|
||||
f"Тип уведомления '{notification_type_code}' не найден или неактивен!"
|
||||
)
|
||||
return None
|
||||
|
||||
@ -73,10 +81,19 @@ class NotificationService:
|
||||
settings = NotificationService.get_user_settings(
|
||||
recipient, notification_type
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[NotificationService.create_notification] Настройки пользователя: "
|
||||
f"enabled={settings.is_enabled}, "
|
||||
f"web={settings.send_web}, "
|
||||
f"email={settings.send_email}, "
|
||||
f"telegram={settings.send_telegram}"
|
||||
)
|
||||
|
||||
if not settings.is_enabled:
|
||||
logger.info(
|
||||
f"[create_notification] Skipped - disabled for user {recipient.id}"
|
||||
logger.warning(
|
||||
f"[NotificationService.create_notification] ⏭️ ПРОПУСК: "
|
||||
f"Уведомления отключены для user={recipient.id}"
|
||||
)
|
||||
return None
|
||||
|
||||
@ -95,10 +112,23 @@ class NotificationService:
|
||||
action_text=action_text,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[NotificationService.create_notification] ✅ Уведомление создано: "
|
||||
f"id={notification.id}, "
|
||||
f"action_url={action_url}"
|
||||
)
|
||||
|
||||
# Отправить если нужно
|
||||
if send_immediately:
|
||||
logger.info(
|
||||
f"[NotificationService.create_notification] ➡️ Немедленная отправка notification_id={notification.id}"
|
||||
)
|
||||
NotificationService.send_notification(notification, settings)
|
||||
else:
|
||||
logger.info(
|
||||
f"[NotificationService.create_notification] ⏸️ Отложенная отправка notification_id={notification.id}"
|
||||
)
|
||||
|
||||
return notification
|
||||
|
||||
@ -109,7 +139,13 @@ class NotificationService:
|
||||
):
|
||||
"""Отправить уведомление по всем каналам"""
|
||||
logger.info(
|
||||
f"[send_notification] Starting for notification={notification.id}"
|
||||
f"\n{'='*80}\n"
|
||||
f"[NotificationService.send_notification] 📤 НАЧАЛО ОТПРАВКИ\n"
|
||||
f" notification_id={notification.id}\n"
|
||||
f" recipient={notification.recipient.id} ({notification.recipient.email})\n"
|
||||
f" type={notification.notification_type.code}\n"
|
||||
f" title='{notification.title}'\n"
|
||||
f"{'='*80}"
|
||||
)
|
||||
|
||||
if settings is None:
|
||||
@ -119,20 +155,44 @@ class NotificationService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[send_notification] Channels: web={settings.send_web} "
|
||||
f"email={settings.send_email} telegram={settings.send_telegram}"
|
||||
f"[NotificationService.send_notification] Активные каналы: "
|
||||
f"{'✅' if settings.send_web else '❌'} Web, "
|
||||
f"{'✅' if settings.send_email else '❌'} Email, "
|
||||
f"{'✅' if settings.send_telegram else '❌'} Telegram"
|
||||
)
|
||||
|
||||
# Отправить на веб (WebSocket)
|
||||
if settings.send_web:
|
||||
NotificationService.send_web_notification(notification)
|
||||
notification.sent_web = True
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] 🌐 Отправка через WebSocket..."
|
||||
)
|
||||
try:
|
||||
NotificationService.send_web_notification(notification)
|
||||
notification.sent_web = True
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] ✅ WebSocket: успешно отправлено"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[NotificationService.send_notification] ❌ WebSocket: ошибка - {e}",
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] ⏭️ WebSocket: канал отключен"
|
||||
)
|
||||
|
||||
# Отправить на email
|
||||
if settings.send_email:
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] 📧 Отправка Email..."
|
||||
)
|
||||
try:
|
||||
recipient_email = notification.recipient.email
|
||||
if recipient_email:
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] Email адрес: {recipient_email}"
|
||||
)
|
||||
success = EmailNotificationSender.send_notification_email(
|
||||
notification=notification,
|
||||
recipient_email=recipient_email,
|
||||
@ -140,25 +200,35 @@ class NotificationService:
|
||||
notification.sent_email = success
|
||||
if success:
|
||||
logger.info(
|
||||
f"Email отправлен для уведомления {notification.id}"
|
||||
f"[NotificationService.send_notification] ✅ Email: успешно отправлено на {recipient_email}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[NotificationService.send_notification] ⚠️ Email: отправка вернула False"
|
||||
)
|
||||
else:
|
||||
notification.sent_email = False
|
||||
logger.warning(
|
||||
f"У пользователя {notification.recipient} нет email"
|
||||
f"[NotificationService.send_notification] ⚠️ Email: у пользователя {notification.recipient} нет email адреса"
|
||||
)
|
||||
except Exception as e:
|
||||
notification.sent_email = False
|
||||
logger.error(
|
||||
f"Ошибка отправки email для {notification.id}: {e}",
|
||||
f"[NotificationService.send_notification] ❌ Email: ошибка отправки - {e}",
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] ⏭️ Email: канал отключен"
|
||||
)
|
||||
|
||||
# Отправить в Telegram
|
||||
if settings.send_telegram:
|
||||
logger.info(
|
||||
f"[Telegram] Attempting to send for notification {notification.id} "
|
||||
f"to user {notification.recipient.id}"
|
||||
f"[NotificationService.send_notification] 💬 Отправка в Telegram..."
|
||||
)
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] Получатель: user_id={notification.recipient.id}"
|
||||
)
|
||||
try:
|
||||
# Проверяем наличие Telegram Chat ID в профиле
|
||||
@ -169,8 +239,7 @@ class NotificationService:
|
||||
telegram_chat_id = telegram_chat_id.strip()
|
||||
|
||||
logger.info(
|
||||
f"[Telegram] Chat ID for user {notification.recipient.id}: "
|
||||
f"'{telegram_chat_id}'"
|
||||
f"[NotificationService.send_notification] Telegram Chat ID: '{telegram_chat_id}'"
|
||||
)
|
||||
|
||||
if telegram_chat_id:
|
||||
@ -183,8 +252,7 @@ class NotificationService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[Telegram] Sending via TelegramNotificationSender "
|
||||
f"to chat_id={telegram_chat_id}"
|
||||
f"[NotificationService.send_notification] ➡️ Вызов TelegramNotificationSender.send_notification"
|
||||
)
|
||||
|
||||
# Отправляем через Bot API по chat_id
|
||||
@ -198,27 +266,28 @@ class NotificationService:
|
||||
|
||||
if success:
|
||||
logger.info(
|
||||
f"[Telegram] Successfully sent notification "
|
||||
f"{notification.id}"
|
||||
f"[NotificationService.send_notification] ✅ Telegram: успешно отправлено (chat_id={telegram_chat_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[Telegram] Failed to send notification "
|
||||
f"{notification.id}"
|
||||
f"[NotificationService.send_notification] ⚠️ Telegram: отправка вернула False"
|
||||
)
|
||||
else:
|
||||
notification.sent_telegram = False
|
||||
logger.debug(
|
||||
f"[Telegram] Chat ID empty for user "
|
||||
f"{notification.recipient.id}"
|
||||
logger.warning(
|
||||
f"[NotificationService.send_notification] ⚠️ Telegram: у пользователя нет Chat ID"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
notification.sent_telegram = False
|
||||
logger.error(
|
||||
f"[Telegram] Error sending notification {notification.id}: {e}",
|
||||
f"[NotificationService.send_notification] ❌ Telegram: ошибка - {e}",
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"[NotificationService.send_notification] ⏭️ Telegram: канал отключен"
|
||||
)
|
||||
|
||||
# Обновить время отправки
|
||||
notification.sent_at = timezone.now()
|
||||
@ -230,10 +299,27 @@ class NotificationService:
|
||||
'sent_wechat',
|
||||
'sent_at'
|
||||
])
|
||||
|
||||
logger.info(
|
||||
f"\n{'='*80}\n"
|
||||
f"[NotificationService.send_notification] 🎯 ЗАВЕРШЕНО\n"
|
||||
f" notification_id={notification.id}\n"
|
||||
f" Результаты: "
|
||||
f"Web={'\u2705' if notification.sent_web else '\u274c'}, "
|
||||
f"Email={'\u2705' if notification.sent_email else '\u274c'}, "
|
||||
f"Telegram={'\u2705' if notification.sent_telegram else '\u274c'}\n"
|
||||
f"{'='*80}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def send_web_notification(notification: Notification):
|
||||
"""Отправить уведомление через WebSocket"""
|
||||
logger.info(
|
||||
f"[NotificationService.send_web_notification] НАЧАЛО: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={notification.recipient.id}"
|
||||
)
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
notification_data = {
|
||||
'id': notification.id,
|
||||
@ -248,14 +334,23 @@ class NotificationService:
|
||||
'action_text': notification.action_text,
|
||||
'created_at': notification.created_at.isoformat(),
|
||||
}
|
||||
|
||||
group_name = f'notifications_{notification.recipient.id}'
|
||||
logger.info(
|
||||
f"[NotificationService.send_web_notification] Отправка в группу: {group_name}"
|
||||
)
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'notifications_{notification.recipient.id}',
|
||||
group_name,
|
||||
{
|
||||
'type': 'notification_new',
|
||||
'notification': notification_data
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[NotificationService.send_web_notification] ✅ Уведомление отправлено в WebSocket"
|
||||
)
|
||||
|
||||
# Обновить счетчик
|
||||
unread_count = Notification.objects.filter(
|
||||
@ -263,14 +358,22 @@ class NotificationService:
|
||||
is_read=False,
|
||||
is_archived=False
|
||||
).count()
|
||||
|
||||
logger.info(
|
||||
f"[NotificationService.send_web_notification] Обновление счетчика: {unread_count} непрочитанных"
|
||||
)
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'notifications_{notification.recipient.id}',
|
||||
group_name,
|
||||
{
|
||||
'type': 'notification_count_update',
|
||||
'count': unread_count
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[NotificationService.send_web_notification] ✅ Счетчик обновлен"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_user_settings(
|
||||
|
||||
@ -58,8 +58,17 @@ class TelegramNotificationSender:
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender.send_notification_async] НАЧАЛО: "
|
||||
f"telegram_id={telegram_id}, "
|
||||
f"notification_id={notification.id}"
|
||||
)
|
||||
|
||||
bot = cls.get_bot()
|
||||
if not bot:
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ Bot не инициализирован (токен отсутствует)"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -68,16 +77,28 @@ class TelegramNotificationSender:
|
||||
category_name = notification.notification_type.category.name
|
||||
emoji = cls.CATEGORY_EMOJI.get(category_code, '🔔')
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Категория: {category_name} ({category_code}), emoji: {emoji}"
|
||||
)
|
||||
|
||||
# Формируем текст сообщения
|
||||
text = f"{emoji} <b>{notification.title}</b>\n\n"
|
||||
text += f"{notification.message}\n\n"
|
||||
text += f"<i>Категория: {category_name}</i>"
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Текст сообщения сформирован (длина: {len(text)} символов)"
|
||||
)
|
||||
|
||||
# Создаем кнопку действия если есть URL
|
||||
keyboard = None
|
||||
if notification.action_url:
|
||||
action_text = notification.action_text or 'Посмотреть'
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Формирование кнопки: action_url={notification.action_url}"
|
||||
)
|
||||
|
||||
# Формируем полный URL
|
||||
if site_url:
|
||||
if notification.action_url.startswith('http'):
|
||||
@ -108,11 +129,21 @@ class TelegramNotificationSender:
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton(action_text, url=url)]
|
||||
])
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ✅ Inline кнопка создана: {url}"
|
||||
)
|
||||
elif 'localhost' in url.lower() or '127.0.0.1' in url:
|
||||
# Добавляем URL в текст сообщения
|
||||
text += f"\n\n🔗 Ссылка: {url}"
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ⚠️ localhost URL добавлен в текст (inline кнопка невозможна)"
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ➡️ Отправка в Telegram API (chat_id={telegram_id})..."
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=telegram_id,
|
||||
text=text,
|
||||
@ -122,25 +153,35 @@ class TelegramNotificationSender:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Telegram уведомление отправлено: {notification.id} -> {telegram_id}"
|
||||
f"[TelegramNotificationSender] ✅ Telegram API УСПЕШНО: "
|
||||
f"notification_id={notification.id} -> chat_id={telegram_id}"
|
||||
)
|
||||
return True
|
||||
|
||||
except TelegramError as e:
|
||||
error_msg = str(e)
|
||||
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ TelegramError: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"chat_id={telegram_id}, "
|
||||
f"error={error_msg}"
|
||||
)
|
||||
|
||||
# Обработка специфичных ошибок
|
||||
if "chat not found" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"Пользователь с Chat ID {telegram_id} не отправил /start боту"
|
||||
f"[TelegramNotificationSender] ⚠️ Чат не найден: "
|
||||
f"пользователь с Chat ID {telegram_id} не отправил /start боту"
|
||||
)
|
||||
elif "bot was blocked" in error_msg.lower() or "forbidden" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"Пользователь с Chat ID {telegram_id} заблокировал бота"
|
||||
f"[TelegramNotificationSender] ⚠️ Бот заблокирован: "
|
||||
f"пользователь с Chat ID {telegram_id} заблокировал бота"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Ошибка отправки Telegram уведомления {notification.id}: {e}",
|
||||
f"[TelegramNotificationSender] ❌ Необработанная ошибка Telegram: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
@ -171,39 +212,67 @@ class TelegramNotificationSender:
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender.send_notification] НАЧАЛО: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"chat_id={chat_id}, "
|
||||
f"site_url={site_url}"
|
||||
)
|
||||
|
||||
# Преобразуем chat_id в int
|
||||
if not chat_id:
|
||||
logger.warning(
|
||||
f"Chat ID не указан для {notification.recipient}"
|
||||
f"[TelegramNotificationSender] ❌ Chat ID не указан для {notification.recipient}"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
telegram_id = int(chat_id)
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Chat ID преобразован в int: {telegram_id}"
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
logger.error(
|
||||
f"Неверный формат Chat ID: {chat_id}"
|
||||
f"[TelegramNotificationSender] ❌ Неверный формат Chat ID: {chat_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Получить site_url если не передан
|
||||
if not site_url:
|
||||
site_url = getattr(settings, 'SITE_URL', 'http://localhost:9000')
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Site URL из настроек: {site_url}"
|
||||
)
|
||||
|
||||
# Запускаем асинхронную отправку
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ➡️ Запуск асинхронной отправки..."
|
||||
)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
return loop.run_until_complete(
|
||||
result = loop.run_until_complete(
|
||||
cls.send_notification_async(
|
||||
telegram_id,
|
||||
notification,
|
||||
site_url
|
||||
)
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ✅ Успешно отправлено в Telegram (chat_id={telegram_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[TelegramNotificationSender] ⚠️ Отправка в Telegram вернула False"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def send_notification_by_username(
|
||||
|
||||
@ -9,6 +9,7 @@ Signals для автоматической генерации уведомле
|
||||
- Изменение статуса заявления
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||
@ -18,6 +19,7 @@ from notifications.services import NotificationService
|
||||
from .models import Request, RequestComment
|
||||
|
||||
Employee = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Флаг для отслеживания новых заявлений, ожидающих уведомления
|
||||
@ -201,24 +203,38 @@ def notify_new_request(request_obj):
|
||||
|
||||
При sent_to_all_department=True отправляет всем сотрудникам отделов
|
||||
"""
|
||||
logger.info(
|
||||
f"\n{'='*80}\n"
|
||||
f"[notify_new_request] 📨 НАЧАЛО ОТПРАВКИ УВЕДОМЛЕНИЙ О НОВОМ ЗАЯВЛЕНИИ\n"
|
||||
f" Request ID: {request_obj.id}\n"
|
||||
f" Тип: {request_obj.get_type_display()}\n"
|
||||
f" Сотрудник: {request_obj.employee}\n"
|
||||
f" Статус: {request_obj.status}\n"
|
||||
f"{'='*80}"
|
||||
)
|
||||
|
||||
print(f"📨 [notify_new_request] Начинаем обработку заявления #{request_obj.id}")
|
||||
|
||||
recipients_set = set()
|
||||
|
||||
# 1. Основные получатели
|
||||
recipients_count = request_obj.recipients.count()
|
||||
logger.info(f"[notify_new_request] Основные получатели (recipients): {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} (ID={recipient.id}, email={recipient.email})")
|
||||
print(
|
||||
f" ✅ Добавлен основной получатель: {recipient.username} (ID: {recipient.id})"
|
||||
)
|
||||
|
||||
# 2. Копия (CC)
|
||||
cc_count = request_obj.cc_users.count()
|
||||
logger.info(f"[notify_new_request] CC users: {cc_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} (ID={cc_user.id}, email={cc_user.email})")
|
||||
print(f" ✅ Добавлен CC: {cc_user.username} (ID: {cc_user.id})")
|
||||
|
||||
# 3. Если sent_to_all_department - все сотрудники отделов
|
||||
@ -269,16 +285,23 @@ def notify_new_request(request_obj):
|
||||
recipients_set.update(dept_processors)
|
||||
|
||||
# Итоговое логирование
|
||||
logger.info(
|
||||
f"[notify_new_request] 📊 ИТОГО получателей: {len(recipients_set)}"
|
||||
)
|
||||
print(
|
||||
f"📊 [notify_new_request] Всего получателей для заявления #{request_obj.id}: {len(recipients_set)}"
|
||||
)
|
||||
if len(recipients_set) == 0:
|
||||
logger.warning(
|
||||
f"[notify_new_request] ⚠️ НЕТ ПОЛУЧАТЕЛЕЙ! Уведомления не отправлены для request_id={request_obj.id}"
|
||||
)
|
||||
print(
|
||||
f"⚠️ [notify_new_request] НЕТ ПОЛУЧАТЕЛЕЙ! Уведомления не будут отправлены."
|
||||
)
|
||||
return
|
||||
|
||||
for r in recipients_set:
|
||||
logger.info(f"[notify_new_request] 👤 {r.username} (ID={r.id}, email={r.email})")
|
||||
print(f" 👤 {r.username} (ID: {r.id})")
|
||||
|
||||
# Определяем тип уведомления для каждого получателя
|
||||
@ -287,6 +310,10 @@ def notify_new_request(request_obj):
|
||||
comment_preview = (
|
||||
request_obj.comment[:150] if request_obj.comment else "Без комментария"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[notify_new_request] Начало отправки {len(recipients_set)} уведомлений..."
|
||||
)
|
||||
|
||||
for recipient in recipients_set:
|
||||
# Определяем роль получателя
|
||||
@ -294,6 +321,10 @@ 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}: "
|
||||
f"primary={is_primary}, cc={is_cc}, approver={is_approver}"
|
||||
)
|
||||
print(
|
||||
f" 📤 Отправка уведомления для {recipient.username}: "
|
||||
f"primary={is_primary}, cc={is_cc}, approver={is_approver}"
|
||||
@ -319,6 +350,10 @@ def notify_new_request(request_obj):
|
||||
title = f"📝 Новое заявление в отделе от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
notification_type = "request_new" # Используем общий тип
|
||||
|
||||
logger.info(
|
||||
f"[notify_new_request] ➡️ Вызов NotificationService.create_notification для {recipient.username}"
|
||||
)
|
||||
|
||||
NotificationService.create_notification(
|
||||
recipient=recipient,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 🔍 Подробный отчет: Проблемы с тестами аутентификации
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Дата:** 5 января 2026
|
||||
**Файл:** `tests/api/auth/test_auth_and_registration.py`
|
||||
|
||||
## 📊 Статистика
|
||||
@ -19,12 +19,12 @@
|
||||
#### ✅ Модель Employee (models.py:162-165):
|
||||
```python
|
||||
gender = models.PositiveSmallIntegerField(
|
||||
"Пол", choices=GENDER_CHOICES,
|
||||
"Пол", choices=GENDER_CHOICES,
|
||||
default=0, # ✅ Есть дефолт
|
||||
blank=True # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
)
|
||||
avatar = models.ImageField(
|
||||
"Фото", upload_to="users/avatars",
|
||||
"Фото", upload_to="users/avatars",
|
||||
blank=True, # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
null=True # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
)
|
||||
@ -34,9 +34,9 @@ avatar = models.ImageField(
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
|
||||
avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!)
|
||||
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=True, # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!)
|
||||
choices=((1, "Мужской"), (2, "Женский")),
|
||||
@ -57,9 +57,9 @@ class RegisterSerializer(serializers.Serializer):
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
|
||||
avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ
|
||||
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=True, # ❌ ОБЯЗАТЕЛЬНОЕ
|
||||
choices=((1, "Мужской"), (2, "Женский")),
|
||||
@ -105,15 +105,15 @@ def register_payload(**overrides):
|
||||
## 📋 Детальный список проваленных тестов
|
||||
|
||||
### 1. ❌ test_register_success_sends_email_and_user_inactive
|
||||
**Цель:** Проверка успешной регистрации с отправкой email
|
||||
**Цель:** Проверка успешной регистрации с отправкой email
|
||||
**Проблема:** Не передает `avatar` и `gender`
|
||||
|
||||
### 2. ❌ test_register_duplicate_email_phone
|
||||
**Цель:** Проверка дубликатов email/phone
|
||||
**Цель:** Проверка дубликатов email/phone
|
||||
**Проблема:** Не может создать первого пользователя (нет `avatar`, `gender`)
|
||||
|
||||
### 3. ❌ test_register_accepts_optional_fields
|
||||
**Цель:** Проверка опциональных полей (patronymic, whatsapp)
|
||||
**Цель:** Проверка опциональных полей (patronymic, whatsapp)
|
||||
**Проблема:** Передает `gender=1`, но **не передает `avatar`**
|
||||
|
||||
```python
|
||||
@ -175,7 +175,7 @@ gender = serializers.ChoiceField(
|
||||
)
|
||||
```
|
||||
|
||||
**Плюсы:** Упрощает регистрацию
|
||||
**Плюсы:** Упрощает регистрацию
|
||||
**Минусы:** Может нарушить бизнес-логику
|
||||
|
||||
#### Вариант B: Исправить тесты (добавить поля)
|
||||
@ -195,7 +195,7 @@ def register_payload(**overrides):
|
||||
return data
|
||||
```
|
||||
|
||||
**Плюсы:** Соответствует текущей бизнес-логике
|
||||
**Плюсы:** Соответствует текущей бизнес-логике
|
||||
**Минусы:** Нужно обновить все тесты
|
||||
|
||||
## 🎯 Рекомендации
|
||||
@ -209,14 +209,14 @@ def register_payload(**overrides):
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
|
||||
# Было:
|
||||
# avatar = Base64ImageField(required=True)
|
||||
# gender = serializers.ChoiceField(required=True, ...)
|
||||
|
||||
|
||||
# Стало:
|
||||
avatar = Base64ImageField(required=False, allow_null=True) # ✅ Соответствует модели
|
||||
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=False, # ✅ Соответствует модели
|
||||
choices=((1, "Мужской"), (2, "Женский"), (0, "Не указано")),
|
||||
@ -246,7 +246,7 @@ def register_payload(**overrides):
|
||||
"data:image/png;base64,"
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
)
|
||||
|
||||
|
||||
data = {
|
||||
"first_name": "Иван",
|
||||
"last_name": "Иванов",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 🔍 Анализ: Восстановление пароля не отправляет email
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** ⚠️ Email не отправляется при восстановлении пароля
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** ⚠️ Email не отправляется при восстановлении пароля
|
||||
**Тип:** Логика фильтрации пользователей в Django PasswordResetForm
|
||||
|
||||
## 📋 Проблема
|
||||
@ -43,7 +43,7 @@ class PasswordResetView(DjangoPasswordResetView):
|
||||
```python
|
||||
def get_users(self, email):
|
||||
"""Given an email, return matching user(s) who should receive a reset.
|
||||
|
||||
|
||||
This allows subclasses to more easily customize the default policies
|
||||
that prevent inactive users and users with unusable passwords from
|
||||
resetting their password.
|
||||
@ -196,21 +196,21 @@ from django.contrib.auth import get_user_model
|
||||
|
||||
class CustomPasswordResetForm(DjangoPasswordResetForm):
|
||||
"""Форма сброса пароля, разрешающая сброс для неактивных пользователей"""
|
||||
|
||||
|
||||
def get_users(self, email):
|
||||
"""Находит пользователей по email, включая неактивных.
|
||||
|
||||
|
||||
Отличие от Django: не фильтрует по is_active=True,
|
||||
чтобы пользователи могли восстановить пароль до верификации email.
|
||||
"""
|
||||
UserModel = get_user_model()
|
||||
email_field_name = UserModel.get_email_field_name()
|
||||
|
||||
|
||||
# Убираем фильтр is_active=True
|
||||
users = UserModel._default_manager.filter(
|
||||
**{f"{email_field_name}__iexact": email}
|
||||
)
|
||||
|
||||
|
||||
return (
|
||||
u for u in users
|
||||
if u.has_usable_password()
|
||||
@ -259,10 +259,10 @@ emp = Employee.objects.create(
|
||||
```python
|
||||
class EmailVerificationRequiredMiddleware:
|
||||
"""Блокирует доступ пользователям без верификации email"""
|
||||
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_authenticated:
|
||||
if not request.user.email_verified:
|
||||
@ -274,7 +274,7 @@ class EmailVerificationRequiredMiddleware:
|
||||
]
|
||||
if not any(request.path.startswith(p) for p in allowed):
|
||||
return redirect('auth_front:verify_email')
|
||||
|
||||
|
||||
return self.get_response(request)
|
||||
```
|
||||
|
||||
@ -370,5 +370,5 @@ send_mail(
|
||||
|
||||
---
|
||||
|
||||
**Приоритет:** 🔴 ВЫСОКИЙ - пользователи не могут восстановить доступ
|
||||
**Приоритет:** 🔴 ВЫСОКИЙ - пользователи не могут восстановить доступ
|
||||
**Трудозатраты:** ⚡ 10-15 минут на реализацию Варианта 1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 🔴 КРИТИЧЕСКАЯ ОШИБКА: Регистрация падает с IntegrityError
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** 🚨 БЛОКИРУЕТ регистрацию пользователей
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** 🚨 БЛОКИРУЕТ регистрацию пользователей
|
||||
**Тип:** Синхронизация БД и модели - несоответствие
|
||||
|
||||
## 📋 Проблема
|
||||
@ -35,7 +35,7 @@ PRAGMA table_info(employees_employee);
|
||||
|
||||
**Пояснение колонок (слева направо):**
|
||||
- `0` = порядковый номер
|
||||
- `'synology_username'` = название колонки
|
||||
- `'synology_username'` = название колонки
|
||||
- `'varchar(100)'` = тип данных
|
||||
- `1` = NOT NULL constraint (1 = есть constraint, 0 = нет)
|
||||
- `None` = default значение
|
||||
@ -46,7 +46,7 @@ PRAGMA table_info(employees_employee);
|
||||
```python
|
||||
class Employee(AbstractUser):
|
||||
# ... другие поля ...
|
||||
|
||||
|
||||
# ❌ SYNOLOGY ПОЛЕЙ НЕТ!
|
||||
# synology_user_id - не определено
|
||||
# synology_username - не определено
|
||||
@ -138,10 +138,10 @@ class Migration(migrations.Migration):
|
||||
```python
|
||||
class Employee(AbstractUser):
|
||||
# ... другие поля ...
|
||||
|
||||
|
||||
synology_user_id = models.IntegerField(null=True, blank=True)
|
||||
synology_username = models.CharField(
|
||||
max_length=100,
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True, # ← Чтобы Django мог вставлять NULL
|
||||
verbose_name="Synology Username"
|
||||
@ -149,7 +149,7 @@ class Employee(AbstractUser):
|
||||
synology_session_expires = models.DateTimeField(null=True, blank=True)
|
||||
synology_session_id = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
blank=True,
|
||||
null=True, # ← Чтобы Django мог вставлять NULL
|
||||
verbose_name="Synology Session ID"
|
||||
)
|
||||
@ -183,7 +183,7 @@ rm backend/db.sqlite3
|
||||
|
||||
Эта ошибка влияет на:
|
||||
- ❌ **Регистрация пользователей** (новые пользователи не могут зарегистрироваться)
|
||||
- ❌ **API endpoint** `/api/v1/auth/register/`
|
||||
- ❌ **API endpoint** `/api/v1/auth/register/`
|
||||
- ❌ **Фронт-end регистрация** (через views_auth.py)
|
||||
- ✅ Всё остальное (логин, профиль) работает нормально
|
||||
|
||||
@ -202,5 +202,5 @@ pytest tests/api/auth/test_auth_and_registration.py::test_register_success_sends
|
||||
|
||||
---
|
||||
|
||||
**Приоритет:** 🔴 КРИТИЧЕСКИЙ - блокирует основной функционал
|
||||
**Приоритет:** 🔴 КРИТИЧЕСКИЙ - блокирует основной функционал
|
||||
**Трудозатраты:** ⚡ 5-10 минут на реализацию + 2-3 минуты на тестирование
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user