627 lines
32 KiB
Python
627 lines
32 KiB
Python
"""
|
||
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'''
|
||
<tr>
|
||
<td style="padding-top: 8px; padding-bottom: 24px; text-align: center;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td style="background-color: #7444FD; border-radius: 4px;">
|
||
<a href="{action_url}" style="display: inline-block; padding: 14px 32px; font-size: 16px; font-weight: 500; color: #FFFFFF; text-decoration: none; border-radius: 4px;">
|
||
Перейти
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
'''
|
||
|
||
email_body = f"""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<!--[if mso]>
|
||
<style type="text/css">
|
||
body, table, td {{font-family: Arial, sans-serif !important;}}
|
||
</style>
|
||
<![endif]-->
|
||
</head>
|
||
<body style="margin: 0; padding: 0; background-color: #FAFAFA; font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #FAFAFA;">
|
||
<tr>
|
||
<td align="center" style="padding: 40px 20px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #FFFFFF; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<tr>
|
||
<td style="padding: 40px 40px 30px 40px; text-align: center;">
|
||
<!-- Стилизованный текстовый логотип uchill -->
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td>
|
||
<span style="display: inline-block; color: #7444FD; letter-spacing: 1px; font-size: 48px; font-weight: 600; line-height: 1.2;">
|
||
<span style="background-color: #7444FD; color: #ffffff; border-radius: 2px; margin-right: 2px; font-weight: 600; font-size: 48px; display: inline-block; vertical-align: baseline; line-height: 1; padding: 2px 8px; letter-spacing: 0;">u</span><span style="vertical-align: baseline;">chill</span>
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 0 40px 40px 40px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="padding-bottom: 24px;">
|
||
<h1 style="margin: 0; font-size: 28px; font-weight: 500; color: #212121; line-height: 1.2;">{notification.title}</h1>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding-bottom: 24px;">
|
||
<p style="margin: 0; font-size: 16px; color: #424242; line-height: 1.6;">{notification.message}</p>
|
||
</td>
|
||
</tr>
|
||
{action_button}
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 30px 40px; background-color: #F5F5F5; border-radius: 0 0 8px 8px; border-top: 1px solid #E0E0E0;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="text-align: center; font-size: 14px; color: #757575; line-height: 1.6;">
|
||
<p style="margin: 0 0 10px 0;">С уважением,<br><strong style="color: #7444FD;">Команда Uchill</strong></p>
|
||
<p style="margin: 10px 0 0 0; font-size: 12px; color: #9E9E9E;">
|
||
© 2026 Uchill. Все права защищены.
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</body>
|
||
</html>
|
||
"""
|
||
else:
|
||
# Если есть шаблон, оборачиваем его в базовую HTML структуру, если нужно
|
||
if not email_body.strip().startswith('<html'):
|
||
action_button = ''
|
||
if notification.action_url:
|
||
action_url = f"{settings.FRONTEND_URL}{notification.action_url}"
|
||
action_button = f'''
|
||
<tr>
|
||
<td style="padding-top: 8px; padding-bottom: 24px; text-align: center;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td style="background-color: #7444FD; border-radius: 4px;">
|
||
<a href="{action_url}" style="display: inline-block; padding: 14px 32px; font-size: 16px; font-weight: 500; color: #FFFFFF; text-decoration: none; border-radius: 4px;">
|
||
Перейти
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
'''
|
||
|
||
email_body = f"""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<!--[if mso]>
|
||
<style type="text/css">
|
||
body, table, td {{font-family: Arial, sans-serif !important;}}
|
||
</style>
|
||
<![endif]-->
|
||
</head>
|
||
<body style="margin: 0; padding: 0; background-color: #FAFAFA; font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #FAFAFA;">
|
||
<tr>
|
||
<td align="center" style="padding: 40px 20px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #FFFFFF; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<tr>
|
||
<td style="padding: 40px 40px 30px 40px; text-align: center;">
|
||
<!-- Стилизованный текстовый логотип uchill -->
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td>
|
||
<span style="display: inline-block; color: #7444FD; letter-spacing: 1px; font-size: 48px; font-weight: 600; line-height: 1.2;">
|
||
<span style="background-color: #7444FD; color: #ffffff; border-radius: 2px; margin-right: 2px; font-weight: 600; font-size: 48px; display: inline-block; vertical-align: baseline; line-height: 1; padding: 2px 8px; letter-spacing: 0;">u</span><span style="vertical-align: baseline;">chill</span>
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 0 40px 40px 40px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="padding-bottom: 24px;">
|
||
{email_body}
|
||
</td>
|
||
</tr>
|
||
{action_button}
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 30px 40px; background-color: #F5F5F5; border-radius: 0 0 8px 8px; border-top: 1px solid #E0E0E0;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="text-align: center; font-size: 14px; color: #757575; line-height: 1.6;">
|
||
<p style="margin: 0 0 10px 0;">С уважением,<br><strong style="color: #7444FD;">Команда Uchill</strong></p>
|
||
<p style="margin: 10px 0 0 0; font-size: 12px; color: #9E9E9E;">
|
||
© 2026 Uchill. Все права защищены.
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
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'''
|
||
<tr>
|
||
<td style="padding-top: 8px; padding-bottom: 24px; text-align: center;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td style="background-color: #7444FD; border-radius: 4px;">
|
||
<a href="{action_url}" style="display: inline-block; padding: 14px 32px; font-size: 16px; font-weight: 500; color: #FFFFFF; text-decoration: none; border-radius: 4px;">
|
||
Перейти
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
'''
|
||
|
||
email_body = f"""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<!--[if mso]>
|
||
<style type="text/css">
|
||
body, table, td {{font-family: Arial, sans-serif !important;}}
|
||
</style>
|
||
<![endif]-->
|
||
</head>
|
||
<body style="margin: 0; padding: 0; background-color: #FAFAFA; font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #FAFAFA;">
|
||
<tr>
|
||
<td align="center" style="padding: 40px 20px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background-color: #FFFFFF; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||
<tr>
|
||
<td style="padding: 40px 40px 30px 40px; text-align: center;">
|
||
<!-- Стилизованный текстовый логотип uchill -->
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
|
||
<tr>
|
||
<td>
|
||
<span style="display: inline-block; color: #7444FD; letter-spacing: 1px; font-size: 48px; font-weight: 600; line-height: 1.2;">
|
||
<span style="background-color: #7444FD; color: #ffffff; border-radius: 2px; margin-right: 2px; font-weight: 600; font-size: 48px; display: inline-block; vertical-align: baseline; line-height: 1; padding: 2px 8px; letter-spacing: 0;">u</span><span style="vertical-align: baseline;">chill</span>
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 0 40px 40px 40px;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="padding-bottom: 24px;">
|
||
<h1 style="margin: 0; font-size: 28px; font-weight: 500; color: #212121; line-height: 1.2;">{notification.title}</h1>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding-bottom: 24px;">
|
||
<p style="margin: 0; font-size: 16px; color: #424242; line-height: 1.6;">{notification.message}</p>
|
||
</td>
|
||
</tr>
|
||
{action_button}
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 30px 40px; background-color: #F5F5F5; border-radius: 0 0 8px 8px; border-top: 1px solid #E0E0E0;">
|
||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="text-align: center; font-size: 14px; color: #757575; line-height: 1.6;">
|
||
<p style="margin: 0 0 10px 0;">С уважением,<br><strong style="color: #7444FD;">Команда Uchill</strong></p>
|
||
<p style="margin: 10px 0 0 0; font-size: 12px; color: #9E9E9E;">
|
||
© 2026 Uchill. Все права защищены.
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
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 уведомления."""
|
||
import re
|
||
import asyncio
|
||
from urllib.parse import urlparse
|
||
|
||
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"<b>{notification.title}</b>\n\n{notification.message}"
|
||
|
||
full_url = None
|
||
if notification.action_url:
|
||
full_url = f"{settings.FRONTEND_URL}{notification.action_url}"
|
||
frontend_domain = urlparse(settings.FRONTEND_URL).netloc or settings.FRONTEND_URL
|
||
message_text += f'\n\n<a href="{full_url}">{frontend_domain}</a>'
|
||
|
||
# Уведомления от ИИ для ментора — с кнопками управления
|
||
reply_markup = None
|
||
is_mentor_ai_homework = (
|
||
getattr(notification.recipient, 'role', None) == 'mentor'
|
||
and notification.notification_type in ('homework_submitted', 'homework_reviewed')
|
||
and notification.action_url
|
||
and ('ИИ' in (notification.title or '') or 'черновик' in (notification.title or '').lower())
|
||
)
|
||
if is_mentor_ai_homework:
|
||
match = re.match(r'/homework/(\d+)/submissions/(\d+)/?', notification.action_url.strip())
|
||
if match:
|
||
submission_id = match.group(2)
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
keyboard = [
|
||
[InlineKeyboardButton("📝 Открыть решение", callback_data=f"mentor_submission_{submission_id}")],
|
||
]
|
||
if full_url:
|
||
keyboard.append([InlineKeyboardButton("🌐 Открыть на сайте", url=full_url)])
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
try:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
if reply_markup:
|
||
from .telegram_bot import send_telegram_message_with_buttons
|
||
success = loop.run_until_complete(
|
||
send_telegram_message_with_buttons(
|
||
telegram_id, message_text, reply_markup, parse_mode='HTML'
|
||
)
|
||
)
|
||
else:
|
||
from .telegram_bot import send_telegram_message
|
||
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':
|
||
# Проверяем что напоминание ещё не отправлено (используем флаг для 1 часа как основной)
|
||
if lesson.reminder_1h_sent:
|
||
logger.info(f'Lesson {lesson_id} reminder already sent, skipping')
|
||
return f'Lesson {lesson_id} reminder already sent'
|
||
NotificationService.send_lesson_reminder(lesson)
|
||
# Отмечаем что напоминание отправлено
|
||
lesson.reminder_1h_sent = True
|
||
lesson.save(update_fields=['reminder_1h_sent'])
|
||
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)}'
|