uchill/backend/apps/notifications/models.py

605 lines
21 KiB
Python
Raw Permalink 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.

"""
Модели уведомлений.
"""
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