feat: add comprehensive logging for notification system tracking

This commit is contained in:
Игорь Надеин 2026-01-05 11:09:50 +03:00
parent 6cebd772f2
commit 80b19dd1b3
7 changed files with 306 additions and 75 deletions

View File

@ -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

View File

@ -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(

View File

@ -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(

View File

@ -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,

View File

@ -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": "Иванов",

View File

@ -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

View File

@ -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 минуты на тестирование