605 lines
21 KiB
Python
605 lines
21 KiB
Python
"""
|
||
Модели уведомлений.
|
||
"""
|
||
from django.db import models
|
||
from django.utils import timezone
|
||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||
from django.contrib.contenttypes.models import ContentType
|
||
|
||
|
||
class PushSubscription(models.Model):
|
||
"""
|
||
Модель для хранения Push Notification subscriptions.
|
||
Используется для отправки push уведомлений в браузер.
|
||
"""
|
||
user = models.ForeignKey(
|
||
'users.User',
|
||
on_delete=models.CASCADE,
|
||
related_name='push_subscriptions',
|
||
verbose_name='Пользователь'
|
||
)
|
||
|
||
subscription_data = models.JSONField(
|
||
verbose_name='Данные subscription',
|
||
help_text='JSON с данными subscription от браузера'
|
||
)
|
||
|
||
user_agent = models.CharField(
|
||
max_length=500,
|
||
blank=True,
|
||
verbose_name='User Agent',
|
||
help_text='Браузер пользователя'
|
||
)
|
||
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Активна'
|
||
)
|
||
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата создания'
|
||
)
|
||
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name='Дата обновления'
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = 'Push Subscription'
|
||
verbose_name_plural = 'Push Subscriptions'
|
||
ordering = ['-created_at']
|
||
# Один пользователь может иметь несколько subscriptions (разные устройства/браузеры)
|
||
unique_together = []
|
||
|
||
def __str__(self):
|
||
return f"Push subscription для {self.user.email}"
|
||
|
||
|
||
class Notification(models.Model):
|
||
"""
|
||
Модель уведомления.
|
||
Универсальная модель для всех типов уведомлений.
|
||
"""
|
||
|
||
TYPE_CHOICES = [
|
||
('lesson_created', 'Создано занятие'),
|
||
('lesson_updated', 'Занятие обновлено'),
|
||
('lesson_cancelled', 'Занятие отменено'),
|
||
('lesson_rescheduled', 'Занятие перенесено'),
|
||
('lesson_reminder', 'Напоминание о занятии'),
|
||
('lesson_started', 'Занятие началось'),
|
||
('lesson_completed', 'Занятие завершено'),
|
||
('homework_assigned', 'Назначено домашнее задание'),
|
||
('homework_submitted', 'ДЗ сдано'),
|
||
('homework_reviewed', 'ДЗ проверено'),
|
||
('homework_returned', 'ДЗ возвращено на доработку'),
|
||
('homework_deadline_reminder', 'Напоминание о дедлайне ДЗ'),
|
||
('homework_overdue', 'ДЗ просрочено'),
|
||
('material_added', 'Добавлен материал'),
|
||
('message_received', 'Новое сообщение'),
|
||
('subscription_expiring', 'Подписка истекает'),
|
||
('subscription_expired', 'Подписка истекла'),
|
||
('payment_received', 'Платеж получен'),
|
||
('mentorship_request_new', 'Новый запрос на менторство'),
|
||
('mentorship_request_accepted', 'Запрос на менторство принят'),
|
||
('mentorship_request_rejected', 'Запрос на менторство отклонён'),
|
||
('mentor_invitation_new', 'Приглашение от ментора'),
|
||
('mentor_invitation_accepted', 'Приглашение принято студентом'),
|
||
('mentor_invitation_rejected', 'Приглашение отклонено студентом'),
|
||
('system', 'Системное уведомление'),
|
||
]
|
||
|
||
CHANNEL_CHOICES = [
|
||
('in_app', 'Внутри приложения'),
|
||
('email', 'Email'),
|
||
('telegram', 'Telegram'),
|
||
]
|
||
|
||
PRIORITY_CHOICES = [
|
||
('low', 'Низкий'),
|
||
('normal', 'Обычный'),
|
||
('high', 'Высокий'),
|
||
('urgent', 'Срочный'),
|
||
]
|
||
|
||
# Получатель
|
||
recipient = models.ForeignKey(
|
||
'users.User',
|
||
on_delete=models.CASCADE,
|
||
related_name='notifications',
|
||
verbose_name='Получатель'
|
||
)
|
||
|
||
# Тип и канал
|
||
notification_type = models.CharField(
|
||
max_length=50,
|
||
choices=TYPE_CHOICES,
|
||
verbose_name='Тип уведомления',
|
||
db_index=True
|
||
)
|
||
|
||
channel = models.CharField(
|
||
max_length=20,
|
||
choices=CHANNEL_CHOICES,
|
||
default='in_app',
|
||
verbose_name='Канал отправки'
|
||
)
|
||
|
||
priority = models.CharField(
|
||
max_length=20,
|
||
choices=PRIORITY_CHOICES,
|
||
default='normal',
|
||
verbose_name='Приоритет'
|
||
)
|
||
|
||
# Содержание
|
||
title = models.CharField(
|
||
max_length=200,
|
||
verbose_name='Заголовок'
|
||
)
|
||
|
||
message = models.TextField(
|
||
verbose_name='Сообщение'
|
||
)
|
||
|
||
# Дополнительные данные (JSON)
|
||
data = models.JSONField(
|
||
default=dict,
|
||
blank=True,
|
||
verbose_name='Дополнительные данные',
|
||
help_text='Дополнительная информация в формате JSON'
|
||
)
|
||
|
||
# Связь с объектом (generic relation)
|
||
content_type = models.ForeignKey(
|
||
ContentType,
|
||
on_delete=models.CASCADE,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Тип объекта'
|
||
)
|
||
|
||
object_id = models.PositiveIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='ID объекта'
|
||
)
|
||
|
||
content_object = GenericForeignKey('content_type', 'object_id')
|
||
|
||
# Ссылка для действия
|
||
action_url = models.CharField(
|
||
max_length=500,
|
||
blank=True,
|
||
verbose_name='Ссылка для действия',
|
||
help_text='URL для перехода при клике на уведомление'
|
||
)
|
||
|
||
# Статус прочтения
|
||
is_read = models.BooleanField(
|
||
default=False,
|
||
verbose_name='Прочитано',
|
||
db_index=True
|
||
)
|
||
|
||
read_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Время прочтения'
|
||
)
|
||
|
||
# Статус отправки
|
||
is_sent = models.BooleanField(
|
||
default=False,
|
||
verbose_name='Отправлено'
|
||
)
|
||
|
||
sent_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Время отправки'
|
||
)
|
||
|
||
send_error = models.TextField(
|
||
blank=True,
|
||
verbose_name='Ошибка отправки'
|
||
)
|
||
|
||
# Временные метки
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата создания',
|
||
db_index=True
|
||
)
|
||
|
||
scheduled_for = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Запланировано на',
|
||
help_text='Время когда нужно отправить уведомление'
|
||
)
|
||
|
||
expires_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Истекает',
|
||
help_text='После этого времени уведомление неактуально'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'notifications'
|
||
verbose_name = 'Уведомление'
|
||
verbose_name_plural = 'Уведомления'
|
||
ordering = ['-created_at']
|
||
indexes = [
|
||
models.Index(fields=['recipient', '-created_at']),
|
||
models.Index(fields=['recipient', 'is_read']),
|
||
models.Index(fields=['notification_type', 'created_at']),
|
||
models.Index(fields=['is_sent', 'scheduled_for']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.get_notification_type_display()} для {self.recipient.email}"
|
||
|
||
def mark_as_read(self):
|
||
"""Отметить как прочитанное."""
|
||
if not self.is_read:
|
||
self.is_read = True
|
||
self.read_at = timezone.now()
|
||
self.save(update_fields=['is_read', 'read_at'])
|
||
# Очищаем кеш дашборда для обновления счетчика непрочитанных уведомлений
|
||
from django.core.cache import cache
|
||
cache_key = f'mentor_dashboard_{self.recipient.id}'
|
||
cache.delete(cache_key)
|
||
# Также очищаем кеш для клиента и родителя, если они есть
|
||
cache.delete(f'client_dashboard_{self.recipient.id}')
|
||
cache.delete(f'parent_dashboard_{self.recipient.id}')
|
||
|
||
def mark_as_sent(self, error=None):
|
||
"""Отметить как отправленное."""
|
||
self.is_sent = True if error is None else False
|
||
self.sent_at = timezone.now()
|
||
if error:
|
||
self.send_error = str(error)
|
||
self.save(update_fields=['is_sent', 'sent_at', 'send_error'])
|
||
|
||
@property
|
||
def is_expired(self):
|
||
"""Проверка истекло ли уведомление."""
|
||
if self.expires_at:
|
||
return timezone.now() > self.expires_at
|
||
return False
|
||
|
||
@classmethod
|
||
def create_notification(cls, recipient, notification_type, title, message,
|
||
channel='in_app', priority='normal', **kwargs):
|
||
"""
|
||
Удобный метод для создания уведомления.
|
||
|
||
Args:
|
||
recipient: Получатель (User)
|
||
notification_type: Тип уведомления
|
||
title: Заголовок
|
||
message: Сообщение
|
||
channel: Канал отправки
|
||
priority: Приоритет
|
||
**kwargs: Дополнительные параметры (data, action_url, content_object и т.д.)
|
||
"""
|
||
return cls.objects.create(
|
||
recipient=recipient,
|
||
notification_type=notification_type,
|
||
title=title,
|
||
message=message,
|
||
channel=channel,
|
||
priority=priority,
|
||
**kwargs
|
||
)
|
||
|
||
|
||
class NotificationPreference(models.Model):
|
||
"""
|
||
Настройки уведомлений пользователя.
|
||
Определяет какие типы уведомлений и через какие каналы получать.
|
||
"""
|
||
|
||
user = models.OneToOneField(
|
||
'users.User',
|
||
on_delete=models.CASCADE,
|
||
related_name='notification_preferences',
|
||
verbose_name='Пользователь'
|
||
)
|
||
|
||
# Глобальные настройки
|
||
enabled = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Уведомления включены'
|
||
)
|
||
|
||
# Каналы
|
||
email_enabled = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Email уведомления'
|
||
)
|
||
|
||
telegram_enabled = models.BooleanField(
|
||
default=False,
|
||
verbose_name='Telegram уведомления'
|
||
)
|
||
|
||
in_app_enabled = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Внутренние уведомления'
|
||
)
|
||
|
||
# Настройки по типам (JSON)
|
||
type_preferences = models.JSONField(
|
||
default=dict,
|
||
blank=True,
|
||
verbose_name='Настройки по типам',
|
||
help_text='Настройки для каждого типа уведомлений'
|
||
)
|
||
|
||
# Время тишины (не отправлять уведомления)
|
||
quiet_hours_enabled = models.BooleanField(
|
||
default=False,
|
||
verbose_name='Режим тишины включен'
|
||
)
|
||
|
||
quiet_hours_start = models.TimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Начало режима тишины'
|
||
)
|
||
|
||
quiet_hours_end = models.TimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Конец режима тишины'
|
||
)
|
||
|
||
# Временные метки
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата создания'
|
||
)
|
||
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name='Дата обновления'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'notification_preferences'
|
||
verbose_name = 'Настройки уведомлений'
|
||
verbose_name_plural = 'Настройки уведомлений'
|
||
|
||
def __str__(self):
|
||
return f"Настройки уведомлений: {self.user.email}"
|
||
|
||
def is_type_enabled(self, notification_type, channel='in_app'):
|
||
"""Проверка включен ли тип уведомления для канала."""
|
||
if not self.enabled:
|
||
return False
|
||
|
||
# Проверка канала
|
||
if channel == 'email' and not self.email_enabled:
|
||
return False
|
||
if channel == 'telegram' and not self.telegram_enabled:
|
||
return False
|
||
if channel == 'in_app' and not self.in_app_enabled:
|
||
return False
|
||
|
||
# Проверка специфичных настроек типа
|
||
if notification_type in self.type_preferences:
|
||
type_settings = self.type_preferences[notification_type]
|
||
if isinstance(type_settings, dict):
|
||
return type_settings.get(channel, True)
|
||
|
||
return True
|
||
|
||
def is_quiet_hours(self):
|
||
"""Проверка находимся ли в режиме тишины."""
|
||
if not self.quiet_hours_enabled or not self.quiet_hours_start or not self.quiet_hours_end:
|
||
return False
|
||
|
||
now = timezone.localtime().time()
|
||
|
||
if self.quiet_hours_start < self.quiet_hours_end:
|
||
# Обычный диапазон (например, 22:00 - 08:00 следующего дня не работает так)
|
||
return self.quiet_hours_start <= now <= self.quiet_hours_end
|
||
else:
|
||
# Диапазон через полночь (например, 22:00 - 08:00)
|
||
return now >= self.quiet_hours_start or now <= self.quiet_hours_end
|
||
|
||
|
||
class NotificationTemplate(models.Model):
|
||
"""
|
||
Шаблон уведомления.
|
||
Хранит шаблоны для разных типов уведомлений.
|
||
"""
|
||
|
||
notification_type = models.CharField(
|
||
max_length=50,
|
||
unique=True,
|
||
verbose_name='Тип уведомления'
|
||
)
|
||
|
||
# Шаблоны для разных каналов
|
||
in_app_title = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name='Заголовок (внутри приложения)'
|
||
)
|
||
|
||
in_app_message = models.TextField(
|
||
blank=True,
|
||
verbose_name='Сообщение (внутри приложения)'
|
||
)
|
||
|
||
email_subject = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name='Тема email'
|
||
)
|
||
|
||
email_body = models.TextField(
|
||
blank=True,
|
||
verbose_name='Тело email (HTML)'
|
||
)
|
||
|
||
telegram_message = models.TextField(
|
||
blank=True,
|
||
verbose_name='Сообщение Telegram'
|
||
)
|
||
|
||
# Переменные шаблона
|
||
variables = models.JSONField(
|
||
default=list,
|
||
blank=True,
|
||
verbose_name='Переменные шаблона',
|
||
help_text='Список доступных переменных для подстановки'
|
||
)
|
||
|
||
# Настройки
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Активен'
|
||
)
|
||
|
||
# Временные метки
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата создания'
|
||
)
|
||
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name='Дата обновления'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'notification_templates'
|
||
verbose_name = 'Шаблон уведомления'
|
||
verbose_name_plural = 'Шаблоны уведомлений'
|
||
|
||
def __str__(self):
|
||
return f"Шаблон: {self.notification_type}"
|
||
|
||
|
||
class ParentChildNotificationSettings(models.Model):
|
||
"""
|
||
Настройки уведомлений родителя для конкретного ребенка.
|
||
Позволяет родителю настроить, какие уведомления получать для каждого ребенка отдельно.
|
||
"""
|
||
|
||
parent = models.ForeignKey(
|
||
'users.Parent',
|
||
on_delete=models.CASCADE,
|
||
related_name='child_notification_settings',
|
||
verbose_name='Родитель'
|
||
)
|
||
|
||
child = models.ForeignKey(
|
||
'users.Client',
|
||
on_delete=models.CASCADE,
|
||
related_name='parent_notification_settings',
|
||
verbose_name='Ребенок'
|
||
)
|
||
|
||
# Общие настройки
|
||
enabled = models.BooleanField(
|
||
default=True,
|
||
verbose_name='Уведомления включены'
|
||
)
|
||
|
||
# Настройки по типам уведомлений (JSON)
|
||
# Формат: {"lesson_created": True, "homework_assigned": False, ...}
|
||
type_settings = models.JSONField(
|
||
default=dict,
|
||
blank=True,
|
||
verbose_name='Настройки по типам',
|
||
help_text='Настройки для каждого типа уведомлений (True/False)'
|
||
)
|
||
|
||
# Временные метки
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата создания'
|
||
)
|
||
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name='Дата обновления'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'parent_child_notification_settings'
|
||
verbose_name = 'Настройки уведомлений родителя для ребенка'
|
||
verbose_name_plural = 'Настройки уведомлений родителей для детей'
|
||
unique_together = [['parent', 'child']]
|
||
indexes = [
|
||
models.Index(fields=['parent', 'child']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"Настройки уведомлений: {self.parent.user.get_full_name()} -> {self.child.user.get_full_name()}"
|
||
|
||
def is_type_enabled(self, notification_type):
|
||
"""Проверка включен ли тип уведомления для этого ребенка."""
|
||
if not self.enabled:
|
||
return False
|
||
|
||
# Если настройка для типа не указана, по умолчанию включено
|
||
if notification_type in self.type_settings:
|
||
return self.type_settings[notification_type]
|
||
|
||
return True
|
||
|
||
def set_type_enabled(self, notification_type, enabled):
|
||
"""Установить настройку для типа уведомления."""
|
||
if not isinstance(self.type_settings, dict):
|
||
self.type_settings = {}
|
||
self.type_settings[notification_type] = enabled
|
||
self.save(update_fields=['type_settings', 'updated_at'])
|
||
|
||
def render(self, channel, context):
|
||
"""
|
||
Рендерить шаблон с контекстом.
|
||
|
||
Args:
|
||
channel: Канал (in_app, email, telegram)
|
||
context: Словарь с переменными для подстановки
|
||
|
||
Returns:
|
||
dict: Словарь с отрендеренным контентом
|
||
"""
|
||
if channel == 'in_app':
|
||
title = self._render_template(self.in_app_title, context)
|
||
message = self._render_template(self.in_app_message, context)
|
||
return {'title': title, 'message': message}
|
||
|
||
elif channel == 'email':
|
||
subject = self._render_template(self.email_subject, context)
|
||
body = self._render_template(self.email_body, context)
|
||
return {'subject': subject, 'body': body}
|
||
|
||
elif channel == 'telegram':
|
||
message = self._render_template(self.telegram_message, context)
|
||
return {'message': message}
|
||
|
||
return {}
|
||
|
||
def _render_template(self, template, context):
|
||
"""Простая замена переменных в шаблоне."""
|
||
if not template:
|
||
return ''
|
||
|
||
result = template
|
||
for key, value in context.items():
|
||
placeholder = '{' + key + '}'
|
||
result = result.replace(placeholder, str(value))
|
||
|
||
return result
|