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