1111 lines
51 KiB
Python
1111 lines
51 KiB
Python
"""
|
||
Сервисы для уведомлений и Telegram интеграции.
|
||
"""
|
||
import secrets
|
||
import string
|
||
from django.core.cache import cache
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
import logging
|
||
from channels.layers import get_channel_layer
|
||
from asgiref.sync import async_to_sync
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class TelegramLinkService:
|
||
"""Сервис для связывания Telegram аккаунтов."""
|
||
|
||
@staticmethod
|
||
def generate_link_code(user_id: int) -> str:
|
||
"""
|
||
Генерация кода для связывания Telegram аккаунта.
|
||
|
||
Args:
|
||
user_id: ID пользователя
|
||
|
||
Returns:
|
||
str: Код связывания (6 символов)
|
||
"""
|
||
# Генерируем случайный код из цифр и букв
|
||
code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(6))
|
||
|
||
# Сохраняем в кэш на 15 минут
|
||
cache_key = f'telegram_link_{code}'
|
||
cache.set(cache_key, user_id, timeout=900) # 15 минут
|
||
|
||
logger.info(f"Generated link code {code} for user {user_id}")
|
||
|
||
return code
|
||
|
||
@staticmethod
|
||
def link_account(link_code: str, telegram_id: int, telegram_username: str = '') -> dict:
|
||
"""
|
||
Связывание Telegram аккаунта с аккаунтом на платформе.
|
||
|
||
Args:
|
||
link_code: Код связывания
|
||
telegram_id: ID пользователя в Telegram
|
||
telegram_username: Username в Telegram
|
||
|
||
Returns:
|
||
dict: Результат связывания
|
||
"""
|
||
from apps.users.models import User
|
||
|
||
# Проверяем код в кэше
|
||
cache_key = f'telegram_link_{link_code}'
|
||
user_id = cache.get(cache_key)
|
||
|
||
if not user_id:
|
||
return {
|
||
'success': False,
|
||
'error': 'Неверный или истекший код связывания'
|
||
}
|
||
|
||
try:
|
||
# Находим пользователя
|
||
user = User.objects.get(id=user_id)
|
||
|
||
# Проверяем не привязан ли уже этот Telegram ID к другому аккаунту
|
||
existing_user = User.objects.filter(telegram_id=telegram_id).first()
|
||
if existing_user and existing_user.id != user.id:
|
||
return {
|
||
'success': False,
|
||
'error': 'Этот Telegram аккаунт уже привязан к другому пользователю'
|
||
}
|
||
|
||
# Связываем аккаунты
|
||
user.telegram_id = telegram_id
|
||
user.telegram_username = telegram_username
|
||
user.save(update_fields=['telegram_id', 'telegram_username'])
|
||
|
||
# Удаляем код из кэша
|
||
cache.delete(cache_key)
|
||
|
||
# Включаем Telegram уведомления
|
||
try:
|
||
preferences = user.notification_preferences
|
||
if not preferences.telegram_enabled:
|
||
preferences.telegram_enabled = True
|
||
preferences.save(update_fields=['telegram_enabled'])
|
||
except:
|
||
# Если нет настроек, создаем
|
||
create_notification_preferences(user)
|
||
|
||
logger.info(f"Linked Telegram {telegram_id} to user {user.id}")
|
||
|
||
return {
|
||
'success': True,
|
||
'user_name': user.get_full_name() or user.email,
|
||
'user_email': user.email
|
||
}
|
||
|
||
except User.DoesNotExist:
|
||
return {
|
||
'success': False,
|
||
'error': 'Пользователь не найден'
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Error linking Telegram account: {e}")
|
||
return {
|
||
'success': False,
|
||
'error': 'Ошибка связывания аккаунта'
|
||
}
|
||
|
||
@staticmethod
|
||
def unlink_account(telegram_id: int) -> dict:
|
||
"""
|
||
Отвязка Telegram аккаунта.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
|
||
Returns:
|
||
dict: Результат отвязки
|
||
"""
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
user = User.objects.get(telegram_id=telegram_id)
|
||
|
||
user.telegram_id = None
|
||
user.telegram_username = ''
|
||
user.save(update_fields=['telegram_id', 'telegram_username'])
|
||
|
||
# Выключаем Telegram уведомления
|
||
try:
|
||
preferences = user.notification_preferences
|
||
preferences.telegram_enabled = False
|
||
preferences.save(update_fields=['telegram_enabled'])
|
||
except:
|
||
pass
|
||
|
||
logger.info(f"Unlinked Telegram {telegram_id} from user {user.id}")
|
||
|
||
return {
|
||
'success': True,
|
||
'message': 'Аккаунт успешно отвязан'
|
||
}
|
||
|
||
except User.DoesNotExist:
|
||
return {
|
||
'success': False,
|
||
'error': 'Аккаунт не найден'
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Error unlinking Telegram account: {e}")
|
||
return {
|
||
'success': False,
|
||
'error': 'Ошибка отвязки аккаунта'
|
||
}
|
||
|
||
|
||
class NotificationService:
|
||
"""Сервис для работы с уведомлениями."""
|
||
|
||
@staticmethod
|
||
def create_notification(recipient, notification_type, title, message, **kwargs):
|
||
"""
|
||
Создание уведомления.
|
||
|
||
Args:
|
||
recipient: Получатель (User)
|
||
notification_type: Тип уведомления
|
||
title: Заголовок
|
||
message: Сообщение
|
||
**kwargs: Дополнительные параметры (channel, priority, action_url, content_object и т.д.)
|
||
"""
|
||
from .models import Notification
|
||
from .tasks import send_notification_task
|
||
from .serializers import NotificationSerializer
|
||
|
||
# Получаем канал из kwargs или используем 'in_app' по умолчанию
|
||
channel = kwargs.pop('channel', 'in_app')
|
||
|
||
# Создаем уведомление
|
||
notification = Notification.create_notification(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
channel=channel,
|
||
**kwargs
|
||
)
|
||
|
||
# Отправляем асинхронно (кроме in_app, они уже в БД)
|
||
if channel != 'in_app':
|
||
send_notification_task.delay(notification.id)
|
||
elif channel == 'in_app':
|
||
# Отправляем через WebSocket для real-time уведомлений
|
||
WebSocketNotificationService.send_notification_via_websocket(notification)
|
||
|
||
return notification
|
||
|
||
@staticmethod
|
||
def create_notification_with_telegram(recipient, notification_type, title, message, **kwargs):
|
||
"""
|
||
Создание уведомления с автоматической отправкой в Telegram и Email (если возможно).
|
||
Создает уведомления только если они включены в настройках пользователя.
|
||
|
||
Args:
|
||
recipient: Получатель (User)
|
||
notification_type: Тип уведомления
|
||
title: Заголовок
|
||
message: Сообщение
|
||
**kwargs: Дополнительные параметры (priority, action_url, content_object, child_id и т.д.)
|
||
"""
|
||
from .models import Notification, ParentChildNotificationSettings
|
||
from .tasks import send_notification_task
|
||
|
||
# Проверяем настройки уведомлений родителя для конкретного ребенка
|
||
if recipient.role == 'parent':
|
||
child_id = kwargs.get('child_id') or kwargs.get('data', {}).get('child_id')
|
||
if child_id:
|
||
try:
|
||
from apps.users.models import Client, Parent
|
||
parent = recipient.parent_profile
|
||
if not parent:
|
||
logger.warning(f'Parent profile not found for user {recipient.id}')
|
||
# Продолжаем без фильтрации, если профиль родителя не найден
|
||
else:
|
||
# Получаем ребенка по user_id
|
||
try:
|
||
child = Client.objects.get(user_id=child_id)
|
||
except Client.DoesNotExist:
|
||
logger.warning(f'Child with user_id {child_id} not found')
|
||
# Продолжаем без фильтрации, если ребенок не найден
|
||
child = None
|
||
|
||
if child:
|
||
# Проверяем, что ребенок связан с этим родителем
|
||
if child in parent.children.all():
|
||
# Получаем настройки для этого ребенка
|
||
try:
|
||
child_settings = ParentChildNotificationSettings.objects.get(
|
||
parent=parent,
|
||
child=child
|
||
)
|
||
# Проверяем, включены ли уведомления для этого ребенка
|
||
if not child_settings.enabled:
|
||
logger.info(f'Notifications disabled for parent {recipient.id} and child {child_id}, skipping notification {notification_type}')
|
||
return None
|
||
|
||
# Проверяем, включен ли конкретный тип уведомления для этого ребенка
|
||
if not child_settings.is_type_enabled(notification_type):
|
||
logger.info(f'Notification type {notification_type} disabled for parent {recipient.id} and child {child_id}')
|
||
return None
|
||
except ParentChildNotificationSettings.DoesNotExist:
|
||
# Если настройки не созданы, используем дефолтные (включено)
|
||
logger.debug(f'No notification settings found for parent {recipient.id} and child {child_id}, using defaults (enabled)')
|
||
pass
|
||
else:
|
||
logger.warning(f'Child {child_id} is not associated with parent {recipient.id}')
|
||
# Продолжаем без фильтрации, если ребенок не связан с родителем
|
||
except Exception as e:
|
||
logger.warning(f'Error checking parent-child notification settings: {e}', exc_info=True)
|
||
|
||
# Проверяем настройки уведомлений
|
||
try:
|
||
preferences = recipient.notification_preferences
|
||
except:
|
||
preferences = None
|
||
|
||
# Если уведомления полностью отключены, не создаем ничего
|
||
if preferences and not preferences.enabled:
|
||
logger.info(f'Notifications disabled for user {recipient.id}, skipping notification {notification_type}')
|
||
return None
|
||
|
||
# Создаем in_app уведомление, если включено
|
||
in_app_notification = None
|
||
in_app_enabled = True
|
||
if preferences:
|
||
in_app_enabled = preferences.is_type_enabled(notification_type, 'in_app')
|
||
else:
|
||
# Если нет настроек, создаем по умолчанию
|
||
in_app_enabled = True
|
||
|
||
if in_app_enabled:
|
||
in_app_notification = Notification.create_notification(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
channel='in_app',
|
||
**kwargs
|
||
)
|
||
# Отправляем через WebSocket для real-time уведомлений
|
||
WebSocketNotificationService.send_notification_via_websocket(in_app_notification)
|
||
|
||
# Создаем email уведомление, если включено
|
||
email_enabled = True
|
||
if preferences:
|
||
email_enabled = preferences.is_type_enabled(notification_type, 'email')
|
||
else:
|
||
# Если нет настроек, проверяем есть ли email у пользователя
|
||
email_enabled = bool(recipient.email)
|
||
|
||
if email_enabled and recipient.email:
|
||
# Создаем email уведомление
|
||
email_notification = Notification.create_notification(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
channel='email',
|
||
**kwargs
|
||
)
|
||
# Отправляем асинхронно
|
||
send_notification_task.delay(email_notification.id)
|
||
|
||
# Создаем telegram уведомление, если возможно
|
||
if recipient.telegram_id:
|
||
telegram_enabled = True
|
||
if preferences:
|
||
telegram_enabled = preferences.is_type_enabled(notification_type, 'telegram')
|
||
else:
|
||
# Если нет настроек, считаем что telegram включен по умолчанию
|
||
telegram_enabled = True
|
||
|
||
if telegram_enabled:
|
||
# Создаем telegram уведомление
|
||
telegram_notification = Notification.create_notification(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
channel='telegram',
|
||
**kwargs
|
||
)
|
||
# Отправляем асинхронно
|
||
send_notification_task.delay(telegram_notification.id)
|
||
|
||
return in_app_notification
|
||
|
||
@staticmethod
|
||
def send_lesson_created(lesson):
|
||
"""
|
||
Отправка уведомления о создании занятия.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
"""
|
||
from django.utils import timezone
|
||
import pytz
|
||
|
||
# Уведомление клиенту
|
||
if lesson.client and lesson.client.user:
|
||
# Используем часовой пояс пользователя
|
||
from apps.users.utils import get_user_timezone
|
||
user_timezone = get_user_timezone(lesson.client.user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_timezone)
|
||
else:
|
||
# Если время не aware, считаем что оно в UTC
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_timezone)
|
||
|
||
start_time = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
# Проверяем, является ли занятие постоянным
|
||
is_recurring = getattr(lesson, 'is_recurring', False)
|
||
recurring_text = ' (постоянное занятие)' if is_recurring else ''
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.client.user,
|
||
notification_type='lesson_created',
|
||
title='📅 Новое занятие',
|
||
message=f'Запланировано занятие{recurring_text}: {lesson.title} на {start_time}',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
|
||
# Отправляем уведомление родителям ребенка
|
||
from apps.users.models import Parent
|
||
child_user = lesson.client.user
|
||
child_id = child_user.id
|
||
|
||
# Находим всех родителей этого ребенка
|
||
try:
|
||
child_client = lesson.client
|
||
# Оптимизация: используем list() для кеширования запроса
|
||
parents = list(child_client.parents.select_related('user').all())
|
||
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type='lesson_created',
|
||
title='📅 Новое занятие для вашего ребенка',
|
||
message=f'Запланировано занятие{recurring_text} для {child_user.get_full_name() or child_user.email}: {lesson.title} на {start_time}',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
child_id=child_id,
|
||
data={'child_id': child_id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending notification to parents for lesson {lesson.id}: {e}')
|
||
|
||
@staticmethod
|
||
def send_lesson_cancelled(lesson):
|
||
"""
|
||
Отправка уведомления об отмене занятия.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
"""
|
||
# Уведомление клиенту
|
||
if lesson.client and lesson.client.user:
|
||
child_user = lesson.client.user
|
||
child_id = child_user.id
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=child_user,
|
||
notification_type='lesson_cancelled',
|
||
title='❌ Занятие отменено',
|
||
message=f'Занятие "{lesson.title}" отменено',
|
||
priority='high',
|
||
action_url='/schedule',
|
||
content_object=lesson
|
||
)
|
||
|
||
# Отправляем уведомление родителям ребенка
|
||
try:
|
||
child_client = lesson.client
|
||
# Оптимизация: используем list() для кеширования запроса
|
||
parents = list(child_client.parents.select_related('user').all())
|
||
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type='lesson_cancelled',
|
||
title='❌ Занятие отменено',
|
||
message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} отменено',
|
||
priority='high',
|
||
action_url='/schedule',
|
||
content_object=lesson,
|
||
child_id=child_id,
|
||
data={'child_id': child_id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending notification to parents for cancelled lesson {lesson.id}: {e}')
|
||
|
||
@staticmethod
|
||
def send_lesson_rescheduled(lesson):
|
||
"""Уведомление о переносе занятия."""
|
||
from django.utils import timezone
|
||
import pytz
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(lesson.mentor.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_tz)
|
||
else:
|
||
local_time = timezone.make_aware(lesson.start_time, pytz.UTC).astimezone(user_tz)
|
||
start_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
msg = f'Занятие "{lesson.title}" перенесено на {start_str}'
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.mentor,
|
||
notification_type='lesson_rescheduled',
|
||
title='📅 Занятие перенесено',
|
||
message=msg,
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
if lesson.client and lesson.client.user:
|
||
child_user = lesson.client.user
|
||
child_tz = get_user_timezone(child_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(child_tz)
|
||
else:
|
||
local_time = timezone.make_aware(lesson.start_time, pytz.UTC).astimezone(child_tz)
|
||
start_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=child_user,
|
||
notification_type='lesson_rescheduled',
|
||
title='📅 Занятие перенесено',
|
||
message=f'Занятие "{lesson.title}" перенесено на {start_str}',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
try:
|
||
parents = list(lesson.client.parents.select_related('user').all())
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type='lesson_rescheduled',
|
||
title='📅 Занятие перенесено',
|
||
message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} перенесено на {start_str}',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
child_id=child_user.id,
|
||
data={'child_id': child_user.id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending rescheduled notification to parents for lesson {lesson.id}: {e}')
|
||
|
||
@staticmethod
|
||
def send_lesson_completed(lesson):
|
||
"""Уведомление о завершении занятия."""
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.mentor,
|
||
notification_type='lesson_completed',
|
||
title='✅ Занятие завершено',
|
||
message=f'Занятие "{lesson.title}" завершено',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
if lesson.client and lesson.client.user:
|
||
child_user = lesson.client.user
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=child_user,
|
||
notification_type='lesson_completed',
|
||
title='✅ Занятие завершено',
|
||
message=f'Занятие "{lesson.title}" завершено',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
try:
|
||
parents = list(lesson.client.parents.select_related('user').all())
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type='lesson_completed',
|
||
title='✅ Занятие завершено',
|
||
message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} завершено',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
child_id=child_user.id,
|
||
data={'child_id': child_user.id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending completed notification to parents for lesson {lesson.id}: {e}')
|
||
|
||
@staticmethod
|
||
def send_lesson_deleted(lesson, is_recurring_series=False):
|
||
"""
|
||
Отправка уведомления об удалении занятия.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
is_recurring_series: Если True, отправляет уведомление о удалении цепочки постоянных занятий
|
||
"""
|
||
# Уведомление клиенту
|
||
if lesson.client and lesson.client.user:
|
||
if is_recurring_series:
|
||
# Уведомление об удалении цепочки постоянных занятий
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.client.user,
|
||
notification_type='lesson_cancelled',
|
||
title='🗑️ Постоянные занятия удалены',
|
||
message=f'Цепочка постоянных занятий "{lesson.title}" удалена',
|
||
priority='high',
|
||
action_url='/schedule',
|
||
content_object=lesson
|
||
)
|
||
else:
|
||
# Уведомление об удалении одного занятия
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.client.user,
|
||
notification_type='lesson_cancelled',
|
||
title='🗑️ Занятие удалено',
|
||
message=f'Занятие "{lesson.title}" удалено',
|
||
priority='high',
|
||
action_url='/schedule',
|
||
content_object=lesson
|
||
)
|
||
|
||
@staticmethod
|
||
def send_lesson_reminder(lesson, time_before=None):
|
||
"""
|
||
Отправка напоминания о занятии.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
time_before: Время до занятия (например, "24 часа", "1 час", "15 минут")
|
||
"""
|
||
from django.utils import timezone
|
||
import pytz
|
||
|
||
# Вычисляем время до занятия для сообщения
|
||
now = timezone.now()
|
||
if timezone.is_aware(lesson.start_time):
|
||
time_until = lesson.start_time - now
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
time_until = utc_time - now
|
||
|
||
# Форматируем время до занятия
|
||
if time_before:
|
||
time_text = f"через {time_before}"
|
||
else:
|
||
# Автоматически определяем время до занятия
|
||
total_seconds = int(time_until.total_seconds())
|
||
hours = total_seconds // 3600
|
||
minutes = (total_seconds % 3600) // 60
|
||
|
||
if hours >= 24:
|
||
days = hours // 24
|
||
time_text = f"через {days} {NotificationService._pluralize_days(days)}"
|
||
elif hours > 0:
|
||
time_text = f"через {hours} {NotificationService._pluralize_hours(hours)}"
|
||
elif minutes > 0:
|
||
time_text = f"через {minutes} {NotificationService._pluralize_minutes(minutes)}"
|
||
else:
|
||
time_text = "скоро"
|
||
|
||
# Отправляем ментору
|
||
from apps.users.utils import get_user_timezone
|
||
mentor_timezone = get_user_timezone(lesson.mentor.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
mentor_local_time = lesson.start_time.astimezone(mentor_timezone)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
mentor_local_time = utc_time.astimezone(mentor_timezone)
|
||
|
||
mentor_start_time = mentor_local_time.strftime('%d.%m.%Y в %H:%M')
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.mentor,
|
||
notification_type='lesson_reminder',
|
||
title='⏰ Напоминание о занятии',
|
||
message=f'Занятие "{lesson.title}" начнется {time_text} ({mentor_start_time})',
|
||
priority='high',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
|
||
# Отправляем клиенту
|
||
if lesson.client and lesson.client.user:
|
||
from apps.users.utils import get_user_timezone
|
||
client_timezone = get_user_timezone(lesson.client.user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
client_local_time = lesson.start_time.astimezone(client_timezone)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
client_local_time = utc_time.astimezone(client_timezone)
|
||
|
||
client_start_time = client_local_time.strftime('%d.%m.%Y в %H:%M')
|
||
child_user = lesson.client.user
|
||
child_id = child_user.id
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=child_user,
|
||
notification_type='lesson_reminder',
|
||
title='⏰ Напоминание о занятии',
|
||
message=f'Занятие "{lesson.title}" начнется {time_text} ({client_start_time})',
|
||
priority='high',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
|
||
# Отправляем уведомление родителям ребенка
|
||
try:
|
||
child_client = lesson.client
|
||
# Оптимизация: используем list() для кеширования запроса
|
||
parents = list(child_client.parents.select_related('user').all())
|
||
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type='lesson_reminder',
|
||
title='⏰ Напоминание о занятии для вашего ребенка',
|
||
message=f'{child_user.get_full_name() or child_user.email}: Занятие "{lesson.title}" начнется {time_text} ({client_start_time})',
|
||
priority='high',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
child_id=child_id,
|
||
data={'child_id': child_id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending reminder notification to parents for lesson {lesson.id}: {e}')
|
||
|
||
@staticmethod
|
||
def _pluralize_days(count):
|
||
"""Склонение слова 'день'."""
|
||
if count % 10 == 1 and count % 100 != 11:
|
||
return 'день'
|
||
elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20):
|
||
return 'дня'
|
||
else:
|
||
return 'дней'
|
||
|
||
@staticmethod
|
||
def _pluralize_hours(count):
|
||
"""Склонение слова 'час'."""
|
||
if count % 10 == 1 and count % 100 != 11:
|
||
return 'час'
|
||
elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20):
|
||
return 'часа'
|
||
else:
|
||
return 'часов'
|
||
|
||
@staticmethod
|
||
def _pluralize_minutes(count):
|
||
"""Склонение слова 'минута'."""
|
||
if count % 10 == 1 and count % 100 != 11:
|
||
return 'минуту'
|
||
elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20):
|
||
return 'минуты'
|
||
else:
|
||
return 'минут'
|
||
|
||
@staticmethod
|
||
def send_homework_notification(homework, notification_type='homework_assigned', student=None):
|
||
"""
|
||
Отправка уведомления о домашнем задании.
|
||
|
||
Args:
|
||
homework: Объект домашнего задания
|
||
notification_type: Тип уведомления
|
||
student: Студент (для homework_submitted и homework_reviewed)
|
||
"""
|
||
if notification_type == 'homework_assigned':
|
||
# Уведомление всем назначенным ученикам о новом ДЗ
|
||
recipients = homework.assigned_to.all()
|
||
title = '📝 Новое домашнее задание'
|
||
lesson_title = homework.lesson.title if homework.lesson else homework.title
|
||
message = f'Вам назначено домашнее задание по занятию "{lesson_title}"'
|
||
|
||
# Отправляем уведомление каждому назначенному студенту
|
||
for recipient in recipients:
|
||
child_id = recipient.id
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}',
|
||
content_object=homework
|
||
)
|
||
|
||
# Отправляем уведомление родителям ребенка
|
||
try:
|
||
from apps.users.models import Client, Parent
|
||
child_client = Client.objects.get(user=recipient)
|
||
parents = child_client.parents.all()
|
||
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type=notification_type,
|
||
title='📝 Новое домашнее задание для вашего ребенка',
|
||
message=f'{recipient.get_full_name() or recipient.email}: {message}',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}',
|
||
content_object=homework,
|
||
child_id=child_id,
|
||
data={'child_id': child_id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending notification to parents for homework {homework.id}: {e}')
|
||
return
|
||
|
||
elif notification_type == 'homework_submitted':
|
||
# Уведомление ментору о сданном ДЗ
|
||
if not homework.lesson:
|
||
return
|
||
recipient = homework.lesson.mentor
|
||
student_name = student.get_full_name() if student else 'Ученик'
|
||
title = '📤 Домашнее задание сдано'
|
||
lesson_title = homework.lesson.title if homework.lesson else homework.title
|
||
message = f'{student_name} сдал домашнее задание по занятию "{lesson_title}"'
|
||
|
||
elif notification_type == 'homework_reviewed':
|
||
# Уведомление ученику о проверенном ДЗ
|
||
if not student:
|
||
return
|
||
recipient = student
|
||
child_id = student.id
|
||
title = '✅ Домашнее задание проверено'
|
||
lesson_title = homework.lesson.title if homework.lesson else homework.title
|
||
message = f'Ваше домашнее задание по занятию "{lesson_title}" проверено'
|
||
|
||
else:
|
||
return
|
||
|
||
if recipient:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}',
|
||
content_object=homework
|
||
)
|
||
|
||
# Отправляем уведомление родителям ребенка (только для homework_reviewed и homework_submitted)
|
||
if notification_type in ['homework_reviewed', 'homework_submitted'] and recipient.role == 'client':
|
||
try:
|
||
from apps.users.models import Client, Parent
|
||
child_client = Client.objects.get(user=recipient)
|
||
parents = child_client.parents.all()
|
||
|
||
for parent in parents:
|
||
if parent.user:
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=parent.user,
|
||
notification_type=notification_type,
|
||
title=f'{title} для вашего ребенка',
|
||
message=f'{recipient.get_full_name() or recipient.email}: {message}',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}',
|
||
content_object=homework,
|
||
child_id=child_id,
|
||
data={'child_id': child_id}
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f'Error sending notification to parents for homework {homework.id}: {e}')
|
||
|
||
@staticmethod
|
||
def send_attendance_confirmation_request(lesson):
|
||
"""
|
||
Отправка запроса о подтверждении присутствия студенту.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
"""
|
||
from django.utils import timezone
|
||
import pytz
|
||
|
||
if not lesson.client or not lesson.client.user:
|
||
return
|
||
|
||
student = lesson.client.user
|
||
|
||
# Используем часовой пояс пользователя
|
||
from apps.users.utils import get_user_timezone
|
||
user_timezone = get_user_timezone(student.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_timezone)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_timezone)
|
||
|
||
start_time = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
# Создаем in-app уведомление и отправляем email
|
||
from .models import Notification
|
||
from .tasks import send_notification_task
|
||
|
||
# Создаем in-app уведомление
|
||
notification = Notification.create_notification(
|
||
recipient=student,
|
||
notification_type='lesson_reminder', # Используем существующий тип
|
||
title='❓ Подтверждение присутствия',
|
||
message=f'Будете ли вы присутствовать на занятии "{lesson.title}" {start_time}?',
|
||
channel='in_app',
|
||
priority='high',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
data={
|
||
'lesson_id': lesson.id,
|
||
'requires_attendance_confirmation': True,
|
||
'attendance_yes_url': f'/api/schedule/lessons/{lesson.id}/confirm-attendance/?response=yes',
|
||
'attendance_no_url': f'/api/schedule/lessons/{lesson.id}/confirm-attendance/?response=no',
|
||
}
|
||
)
|
||
|
||
# Отправляем email уведомление, если включено
|
||
try:
|
||
preferences = student.notification_preferences
|
||
email_enabled = preferences.is_type_enabled('lesson_reminder', 'email')
|
||
except:
|
||
email_enabled = bool(student.email)
|
||
|
||
if email_enabled and student.email:
|
||
email_notification = Notification.create_notification(
|
||
recipient=student,
|
||
notification_type='lesson_reminder',
|
||
title='❓ Подтверждение присутствия',
|
||
message=f'Будете ли вы присутствовать на занятии "{lesson.title}" {start_time}?',
|
||
channel='email',
|
||
priority='high',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson,
|
||
)
|
||
send_notification_task.delay(email_notification.id)
|
||
|
||
# Отправляем также в Telegram с кнопками
|
||
if student.telegram_id:
|
||
try:
|
||
preferences = student.notification_preferences
|
||
telegram_enabled = preferences.is_type_enabled('lesson_reminder', 'telegram')
|
||
except:
|
||
telegram_enabled = True
|
||
|
||
if telegram_enabled:
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
from .telegram_bot import send_telegram_message_with_buttons
|
||
import asyncio
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("✅ Да, буду", callback_data=f"attendance_yes_{lesson.id}"),
|
||
InlineKeyboardButton("❌ Нет, не смогу", callback_data=f"attendance_no_{lesson.id}")
|
||
]
|
||
]
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
message_text = (
|
||
f"❓ <b>Подтверждение присутствия</b>\n\n"
|
||
f"Будете ли вы присутствовать на занятии:\n"
|
||
f"<b>{lesson.title}</b>\n"
|
||
f"📅 {start_time}"
|
||
)
|
||
|
||
# Отправляем сообщение с кнопками напрямую
|
||
try:
|
||
# Запускаем асинхронную отправку
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
success = loop.run_until_complete(
|
||
send_telegram_message_with_buttons(
|
||
student.telegram_id,
|
||
message_text,
|
||
reply_markup
|
||
)
|
||
)
|
||
loop.close()
|
||
except Exception as e:
|
||
logger.error(f'Error sending Telegram message with buttons: {e}')
|
||
success = False
|
||
|
||
@staticmethod
|
||
def send_attendance_response_to_mentor(lesson, response):
|
||
"""
|
||
Отправка уведомления ментору о ответе студента на запрос о присутствии.
|
||
|
||
Args:
|
||
lesson: Объект занятия
|
||
response: True если будет присутствовать, False если нет
|
||
"""
|
||
if not lesson.mentor:
|
||
return
|
||
|
||
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
|
||
response_text = "будет присутствовать" if response else "не сможет присутствовать"
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=lesson.mentor,
|
||
notification_type='lesson_reminder',
|
||
title='📢 Ответ о присутствии',
|
||
message=f'{student_name} {response_text} на занятии "{lesson.title}"',
|
||
priority='normal',
|
||
action_url=f'/schedule?lesson={lesson.id}',
|
||
content_object=lesson
|
||
)
|
||
|
||
@staticmethod
|
||
def send_message_notification(message):
|
||
"""
|
||
Отправка уведомлений о новом сообщении всем участникам чата, кроме отправителя.
|
||
|
||
Args:
|
||
message: Экземпляр Message
|
||
"""
|
||
from .models import Notification
|
||
|
||
chat = message.chat
|
||
sender = message.sender
|
||
|
||
# Получаем всех участников чата, кроме отправителя
|
||
participants = chat.participants.exclude(user=sender).select_related('user')
|
||
|
||
# Формируем имя отправителя
|
||
sender_name = sender.get_full_name() if sender else 'Система'
|
||
|
||
# Обрезаем сообщение для уведомления
|
||
content_preview = ''
|
||
if message.content:
|
||
content_preview = message.content[:100]
|
||
if len(message.content) > 100:
|
||
content_preview += '...'
|
||
|
||
# Определяем заголовок в зависимости от типа чата
|
||
if chat.chat_type == 'direct':
|
||
title = f'💬 Новое сообщение от {sender_name}'
|
||
else:
|
||
title = f'💬 {sender_name} в чате "{chat.name or "Групповой чат"}"'
|
||
|
||
# Создаем уведомления для каждого участника
|
||
for participant in participants:
|
||
# Пропускаем если уведомления отключены
|
||
if participant.is_muted:
|
||
continue
|
||
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=participant.user,
|
||
notification_type='message_received',
|
||
title=title,
|
||
message=content_preview or 'Новое сообщение',
|
||
priority='normal',
|
||
action_url=f'/chat?chat={chat.uuid}',
|
||
content_object=message
|
||
)
|
||
|
||
|
||
def create_notification_preferences(user):
|
||
"""
|
||
Создание настроек уведомлений для пользователя.
|
||
|
||
Args:
|
||
user: Пользователь
|
||
"""
|
||
from .models import NotificationPreference
|
||
|
||
try:
|
||
NotificationPreference.objects.get_or_create(
|
||
user=user,
|
||
defaults={
|
||
'enabled': True,
|
||
'email_enabled': True,
|
||
'telegram_enabled': bool(user.telegram_id),
|
||
'in_app_enabled': True,
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error creating notification preferences: {e}")
|
||
|
||
|
||
class WebSocketNotificationService:
|
||
"""Сервис для отправки уведомлений через WebSocket."""
|
||
|
||
@staticmethod
|
||
def send_notification_via_websocket(notification):
|
||
"""
|
||
Отправка уведомления через WebSocket в реальном времени.
|
||
|
||
Args:
|
||
notification: Объект Notification
|
||
"""
|
||
try:
|
||
channel_layer = get_channel_layer()
|
||
if not channel_layer:
|
||
logger.warning("Channel layer not available, skipping WebSocket notification")
|
||
return
|
||
|
||
from .serializers import NotificationSerializer
|
||
from .models import Notification
|
||
|
||
# Сериализуем уведомление
|
||
serializer = NotificationSerializer(notification)
|
||
notification_data = serializer.data
|
||
|
||
# Группа пользователя для уведомлений
|
||
user_group_name = f'notifications_user_{notification.recipient.id}'
|
||
|
||
# Получаем количество непрочитанных уведомлений
|
||
unread_count = Notification.objects.filter(
|
||
recipient=notification.recipient,
|
||
channel='in_app',
|
||
is_read=False
|
||
).count()
|
||
|
||
# Отправляем через channel_layer
|
||
async_to_sync(channel_layer.group_send)(
|
||
user_group_name,
|
||
{
|
||
'type': 'notification_created',
|
||
'notification': notification_data,
|
||
'unread_count': unread_count
|
||
}
|
||
)
|
||
|
||
logger.info(f"WebSocket notification sent to user {notification.recipient.id} (unread: {unread_count})")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error sending WebSocket notification: {e}", exc_info=True)
|
||
|
||
@staticmethod
|
||
def send_nav_badges_updated(user_id):
|
||
"""
|
||
Уведомить пользователя об изменении бейджей нижнего меню (чат, расписание, ДЗ и т.д.).
|
||
Клиент по событию nav_badges_updated перезапросит GET /api/nav-badges/.
|
||
"""
|
||
try:
|
||
channel_layer = get_channel_layer()
|
||
if not channel_layer:
|
||
return
|
||
user_group_name = f'notifications_user_{user_id}'
|
||
async_to_sync(channel_layer.group_send)(
|
||
user_group_name,
|
||
{'type': 'nav_badges_updated'},
|
||
)
|
||
except Exception as e:
|
||
logger.debug("send_nav_badges_updated: %s", e)
|