diff --git a/backend/notifications/email_sender.py b/backend/notifications/email_sender.py index b2dec8ca..347db190 100644 --- a/backend/notifications/email_sender.py +++ b/backend/notifications/email_sender.py @@ -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 diff --git a/backend/notifications/services.py b/backend/notifications/services.py index 3e8a1c9e..a69c7007 100644 --- a/backend/notifications/services.py +++ b/backend/notifications/services.py @@ -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( diff --git a/backend/notifications/telegram_sender.py b/backend/notifications/telegram_sender.py index 42c788cd..757618df 100644 --- a/backend/notifications/telegram_sender.py +++ b/backend/notifications/telegram_sender.py @@ -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} {notification.title}\n\n" text += f"{notification.message}\n\n" text += f"Категория: {category_name}" + 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( diff --git a/backend/requests_app/notification_signals.py b/backend/requests_app/notification_signals.py index e2808695..268c3f58 100644 --- a/backend/requests_app/notification_signals.py +++ b/backend/requests_app/notification_signals.py @@ -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, diff --git a/docs/reports/AUTH_TESTS_ANALYSIS.md b/docs/reports/AUTH_TESTS_ANALYSIS.md index a23dbb44..fd42f63c 100644 --- a/docs/reports/AUTH_TESTS_ANALYSIS.md +++ b/docs/reports/AUTH_TESTS_ANALYSIS.md @@ -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": "Иванов", diff --git a/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md b/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md index 18bf9bfb..8eaafa58 100644 --- a/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md +++ b/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md @@ -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 diff --git a/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md b/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md index 5c558649..47e06d20 100644 --- a/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md +++ b/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md @@ -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 минуты на тестирование