""" Celery задачи для уведомлений. """ from celery import shared_task from django.core.mail import send_mail, EmailMultiAlternatives from django.conf import settings from django.template.loader import render_to_string from django.utils.html import strip_tags from django.utils import timezone import logging logger = logging.getLogger(__name__) @shared_task def send_notification_task(notification_id): """ Отправка уведомления асинхронно. Args: notification_id: ID уведомления для отправки """ from .models import Notification try: notification = Notification.objects.select_related('recipient').get(id=notification_id) # Проверка настроек пользователя try: preferences = notification.recipient.notification_preferences # Проверка включены ли уведомления if not preferences.is_type_enabled(notification.notification_type, notification.channel): notification.mark_as_sent(error='Отключено в настройках') return f'Notification {notification_id} disabled by user preferences' # Проверка режима тишины if preferences.is_quiet_hours(): # Перенести отправку return f'Notification {notification_id} delayed due to quiet hours' except: # Если нет настроек, используем дефолтные pass # Отправка в зависимости от канала if notification.channel == 'email': result = send_email_notification(notification) elif notification.channel == 'telegram': result = send_telegram_notification(notification) elif notification.channel == 'in_app': # Внутренние уведомления только создаются, не отправляются notification.mark_as_sent() result = 'In-app notification created' else: result = f'Unknown channel: {notification.channel}' return result except Notification.DoesNotExist: return f'Notification {notification_id} not found' except Exception as e: logger.error(f'Error sending notification {notification_id}: {str(e)}') try: notification = Notification.objects.get(id=notification_id) notification.mark_as_sent(error=str(e)) except: pass return f'Error: {str(e)}' def send_email_notification(notification): """Отправка Email уведомления.""" try: recipient_email = notification.recipient.email # Пытаемся получить шаблон для этого типа уведомления from .models import NotificationTemplate try: template = NotificationTemplate.objects.get( notification_type=notification.notification_type, is_active=True ) # Подготавливаем контекст для шаблона context = { 'title': notification.title, 'message': notification.message, 'action_url': f"{settings.FRONTEND_URL}{notification.action_url}" if notification.action_url else None, 'recipient_name': notification.recipient.get_full_name() or notification.recipient.email, 'recipient_email': notification.recipient.email, } # Добавляем данные из notification.data, если есть if notification.data: context.update(notification.data) # Рендерим шаблон rendered = template.render('email', context) email_subject = rendered.get('subject', notification.title) email_body = rendered.get('body', '') # Если шаблон не содержит body, используем дефолтный if not email_body: action_button = '' if notification.action_url: action_url = f"{settings.FRONTEND_URL}{notification.action_url}" action_button = f'''
Перейти
''' email_body = f"""
uchill
{action_button}

{notification.title}

{notification.message}

С уважением,
Команда Uchill

© 2026 Uchill. Все права защищены.

""" else: # Если есть шаблон, оборачиваем его в базовую HTML структуру, если нужно if not email_body.strip().startswith('
Перейти
''' email_body = f"""
uchill
{action_button}
{email_body}

С уважением,
Команда Uchill

© 2026 Uchill. Все права защищены.

""" except NotificationTemplate.DoesNotExist: # Если шаблона нет, используем дефолтный email_subject = notification.title action_button = '' if notification.action_url: action_url = f"{settings.FRONTEND_URL}{notification.action_url}" action_button = f'''
Перейти
''' email_body = f"""
uchill
{action_button}

{notification.title}

{notification.message}

С уважением,
Команда Uchill

© 2026 Uchill. Все права защищены.

""" plain_message = strip_tags(email_body) # Отправляем msg = EmailMultiAlternatives( subject=email_subject, body=plain_message, from_email=settings.DEFAULT_FROM_EMAIL, to=[recipient_email] ) msg.attach_alternative(email_body, "text/html") msg.send() notification.mark_as_sent() logger.info(f'Email notification sent to {recipient_email}') return f'Email sent to {recipient_email}' except Exception as e: logger.error(f'Error sending email notification: {str(e)}') notification.mark_as_sent(error=str(e)) raise def send_telegram_notification(notification): """Отправка Telegram уведомления.""" try: telegram_id = notification.recipient.telegram_id if not telegram_id: notification.mark_as_sent(error='Telegram ID not linked') return 'Telegram ID not linked' # Формируем сообщение в HTML формате message_text = f"{notification.title}\n\n{notification.message}" if notification.action_url: full_url = f"{settings.FRONTEND_URL}{notification.action_url}" message_text += f'\n\nПерейти на платформу' # Отправляем через Telegram Bot API from .telegram_bot import send_telegram_message import asyncio try: # Запускаем асинхронную отправку loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) success = loop.run_until_complete( send_telegram_message(telegram_id, message_text, parse_mode='HTML') ) loop.close() if success: notification.mark_as_sent() logger.info(f'Telegram notification sent to {telegram_id}') return f'Telegram notification sent to {telegram_id}' else: notification.mark_as_sent(error='Failed to send message') return 'Failed to send Telegram message' except Exception as e: logger.error(f'Error sending telegram message: {str(e)}') notification.mark_as_sent(error=str(e)) return f'Error: {str(e)}' except Exception as e: logger.error(f'Error sending telegram notification: {str(e)}') notification.mark_as_sent(error=str(e)) raise @shared_task def send_bulk_notifications(notification_ids): """ Массовая отправка уведомлений. Args: notification_ids: Список ID уведомлений """ results = [] for notification_id in notification_ids: result = send_notification_task.delay(notification_id) results.append(result) return f'Scheduled {len(results)} notifications' @shared_task def cleanup_old_notifications(): """ Очистка старых уведомлений по правилам: - прочитанные: удалять если старше 5 дней (по created_at); - непрочитанные: удалять если старше 14 дней (по created_at). Запускается периодически через Celery Beat (ежедневно в 3:00). """ from .models import Notification from datetime import timedelta now = timezone.now() read_threshold = now - timedelta(days=5) unread_threshold = now - timedelta(days=14) deleted_read = Notification.objects.filter( is_read=True, created_at__lt=read_threshold, ).delete()[0] deleted_unread = Notification.objects.filter( is_read=False, created_at__lt=unread_threshold, ).delete()[0] total = deleted_read + deleted_unread logger.info( 'cleanup_old_notifications: deleted read (older than 5 days)=%s, unread (older than 14 days)=%s, total=%s', deleted_read, deleted_unread, total, ) return f'Deleted {total} notifications (read >5d: {deleted_read}, unread >14d: {deleted_unread})' @shared_task def send_scheduled_notifications(): """ Отправка отложенных уведомлений. Запускается каждую минуту через Celery Beat. """ from .models import Notification # Находим уведомления которые нужно отправить notifications = Notification.objects.filter( is_sent=False, scheduled_for__lte=timezone.now() ) count = 0 for notification in notifications: send_notification_task.delay(notification.id) count += 1 return f'Scheduled {count} notifications' @shared_task def send_lesson_notification(lesson_id, notification_type): """ Отправка уведомления о занятии по типу события. Вызывается из schedule.signals. """ from apps.schedule.models import Lesson from .services import NotificationService try: lesson = Lesson.objects.select_related('mentor', 'client', 'client__user').get(id=lesson_id) except Lesson.DoesNotExist: logger.warning(f'Lesson {lesson_id} not found for notification {notification_type}') return f'Lesson {lesson_id} not found' if notification_type == 'lesson_created': NotificationService.send_lesson_created(lesson) elif notification_type == 'lesson_cancelled': NotificationService.send_lesson_cancelled(lesson) elif notification_type == 'lesson_reminder': NotificationService.send_lesson_reminder(lesson) elif notification_type == 'lesson_rescheduled': NotificationService.send_lesson_rescheduled(lesson) elif notification_type == 'lesson_completed': NotificationService.send_lesson_completed(lesson) else: logger.warning(f'Unknown lesson notification type: {notification_type}') return f'Unknown type: {notification_type}' return f'Lesson notification {notification_type} sent for lesson {lesson_id}' @shared_task def send_lesson_reminder(lesson_id, minutes_before=30): """ Отправка напоминания о занятии. Args: lesson_id: ID занятия minutes_before: За сколько минут до занятия напоминать """ from apps.schedule.models import Lesson from .services import NotificationService try: lesson = Lesson.objects.select_related('mentor', 'client', 'client__user').get(id=lesson_id) # Проверяем не отправили ли уже if lesson.reminder_sent: return 'Reminder already sent' # Отправляем напоминания NotificationService.send_lesson_reminder(lesson) # Отмечаем что напоминание отправлено lesson.reminder_sent = True # reminder_sent_at временно отключено # lesson.reminder_sent_at = timezone.now() lesson.save(update_fields=['reminder_sent']) return f'Lesson reminder sent for lesson {lesson_id}' except Lesson.DoesNotExist: return f'Lesson {lesson_id} not found' except Exception as e: logger.error(f'Error sending lesson reminder: {str(e)}') return f'Error: {str(e)}'