"""
Сервисы для уведомлений и 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"❓ Подтверждение присутствия\n\n"
f"Будете ли вы присутствовать на занятии:\n"
f"{lesson.title}\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)