uchill/backend/apps/notifications/services.py

1126 lines
52 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Сервисы для уведомлений и 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, submission=None):
"""
Отправка уведомления о домашнем задании.
Args:
homework: Объект домашнего задания
notification_type: Тип уведомления
student: Студент (для homework_submitted и homework_reviewed)
submission: Объект решения (для 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}" проверено'
if submission:
if hasattr(submission, 'score') and submission.score is not None:
max_score = getattr(homework, 'max_score', None)
if max_score:
message += f'. Оценка: {submission.score}/{max_score}'
else:
message += f'. Оценка: {submission.score}'
feedback = getattr(submission, 'feedback', None)
if feedback:
import html
escaped_feedback = html.escape(feedback)
message += f'\n\n💬 <b>Комментарий:</b>\n{escaped_feedback}'
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)