Corp/backend/docs/guides/NOTIFICATIONS_V2_USAGE.md
lizerk f4fa00d9bd refactor: Удален Telegram из новой системы уведомлений
- Убрана поддержка Telegram из каналов доставки
- Удалено telegram_enabled из UserChannelPreferences
- Обновлена документация
- Сфокусировались на WebSocket, Email и Web Push

Фокус: веб-уведомления, email дайджесты и web push
2026-03-09 16:01:29 +03:00

23 KiB
Raw Blame History

Универсальная система уведомлений для Django

Версия: 2.0
Дата: 9 марта 2026
Основана на: django-notifications-hq архитектуре
Совместимость: Django 5.2+


📋 Оглавление

  1. Обзор
  2. Архитектура
  3. Установка
  4. Быстрый старт
  5. API Reference
  6. Примеры использования
  7. Настройка каналов
  8. Миграция со старой системы

🎯 Обзор

Упрощенная универсальная система уведомлений, которая:

✅ Проста - 2 модели вместо 6
✅ Универсальна - GenericForeignKey для любых объектов
✅ Стандартна - API как в django-notifications-hq
✅ Multi-channel - WebSocket, Email, Push
✅ Переиспользуема - можно использовать в других проектах
✅ Современна - Django 5.2+ совместима


🏗️ Архитектура

Модели

1. Notification (основная модель)

Структура: actor performed verb on action_object at target

Пример:

John (actor) commented (verb) on photo (target)
Mary (actor) liked (verb) your comment (action_object) on post (target)
System (actor) approved (verb) your request (action_object)

Поля:

  • recipient - получатель (User)
  • actor - кто совершил действие (GenericForeignKey)
  • verb - действие (str): "liked", "commented", "approved"
  • action_object - объект действия (GenericForeignKey, опционально)
  • target - целевой объект (GenericForeignKey, опционально)
  • description - человекочитаемое описание
  • action_url - URL для перехода
  • data - дополнительные данные (JSON)
  • unread - непрочитано (bool)
  • public - публичное (bool)
  • deleted - удалено (bool, мягкое удаление)

2. UserChannelPreferences (настройки пользователя)

Один пользователь - одна запись с настройками всех каналов.

Поля:

  • web_enabled - веб уведомления (bool)
  • email_enabled - email уведомления (bool)
  • push_enabled - Web Push уведомления (bool)
  • email_frequency - частота email: instant/daily/weekly
  • dnd_enabled - режим "Не беспокоить" (bool)
  • dnd_start_time / dnd_end_time - время DND
  • disabled_verbs - список отключенных типов (JSON array)

Компоненты

  1. signals_new.py - notify.send() API для создания уведомлений
  2. channels.py - роутинг по каналам доставки (WebSocket, Email, Push)
  3. models_new.py - модели с QuerySet методами
  4. admin_new.py - Django Admin интерфейс

📦 Установка

Шаг 1: Применить миграции

python manage.py makemigrations notifications
python manage.py migrate notifications

Шаг 2: Обновить код проекта

Код уже готов к использованию! Файлы:

  • backend/notifications/models_new.py
  • backend/notifications/signals_new.py
  • backend/notifications/channels.py
  • backend/notifications/admin_new.py

Шаг 3: Подключить в apps.py

# backend/notifications/apps.py
class NotificationsConfig(AppConfig):
    ...
    
    def ready(self):
        # Новые сигналы
        import notifications.signals_new  # noqa
        import notifications.channels  # noqa

🚀 Быстрый старт

Создание уведомления

from notifications.signals_new import notify

# Простое уведомление
notify.send(
    sender=current_user,
    recipient=other_user,
    verb='liked',
    description='liked your photo'
)

# С объектами
notify.send(
    sender=author,
    recipient=post.author,
    verb='commented',
    action_object=comment,
    target=post,
    description=f'commented on your post "{post.title}"',
    action_url=f'/posts/{post.id}/#comment-{comment.id}',
)

# Множественные получатели
notify.send(
    sender=manager,
    recipient=[user1, user2, user3],
    verb='approved',
    action_object=request,
    description='approved your budget request',
    action_url=f'/requests/{request.id}/',
)

# С дополнительными данными
notify.send(
    sender=system,
    recipient=user,
    verb='reminder',
    description='Meeting in 15 minutes',
    data={
        'meeting_id': 123,
        'meeting_title': 'Sprint Planning',
        'priority': 'high',
    },
    action_url='/calendar/meetings/123/',
)

Получение уведомлений

# Все уведомления пользователя
notifications = user.notifications.all()

# Непрочитанные
unread = user.notifications.unread()

# Прочитанные
read = user.notifications.read()

# По типу (verb)
likes = user.notifications.filter(verb='liked')

# За последние 7 дней
from datetime import timedelta
from django.utils import timezone

recent = user.notifications.filter(
    timestamp__gte=timezone.now() - timedelta(days=7)
)

Управление уведомлениями

# Отметить как прочитанное
notification.mark_as_read()

# Отметить как непрочитанное
notification.mark_as_unread()

# Отметить все как прочитанные
user.notifications.mark_all_as_read()

# Удалить (мягкое удаление)
notification.deleted = True
notification.save()

# Получить только активные
active = user.notifications.active()

📚 API Reference

notify.send()

Создает уведомление и отправляет по каналам.

Сигнатура:

notify.send(
    sender,                # Объект-актор (кто создал)
    recipient,             # User или [User, ...] (кому)
    verb,                  # str (обязательно) - действие
    action_object=None,    # Объект действия (опционально)
    target=None,           # Целевой объект (опционально)
    description='',        # str - описание
    action_url='',         # str - URL для перехода
    data={},               # dict - дополнительные данные
    public=True,           # bool - публичное ли
    timestamp=None,        # datetime - время (по умолчанию now)
)

Возвращает:

  • Notification - если один получатель
  • [Notification, ...] - если несколько получателей

QuerySet методы

# Фильтрация
.unread()               # Непрочитанные
.read()                 # Прочитанные
.active()               # Активные (не удаленные)
.deleted()              # Удаленные
.public()               # Публичные
.for_user(user)         # Для конкретного пользователя

# Действия
.mark_all_as_read(recipient=None)    # Отметить все как прочитанные
.mark_all_as_unread(recipient=None)  # Отметить все как непрочитанные

Модель Notification

Методы:

.mark_as_read()         # Отметить как прочитанное
.mark_as_unread()       # Отметить как непрочитанное

Свойства:

.timesince              # "5 минут назад"
.slug                   # "123-liked-456" (для URL)

Модель UserChannelPreferences

Методы:

.is_verb_enabled(verb)       # Проверить, включен ли тип
.disable_verb(verb)          # Отключить тип
.enable_verb(verb)           # Включить тип
.is_in_dnd_period()          # В режиме "Не беспокоить"?

💡 Примеры использования

Уведомления о комментариях

# signals.py в приложении comments
from django.db.models.signals import post_save
from django.dispatch import receiver
from notifications.signals_new import notify

@receiver(post_save, sender=Comment)
def notify_comment(sender, instance, created, **kwargs):
    if not created:
        return
    
    comment = instance
    post = comment.post
    
    # Уведомить автора поста
    if comment.author != post.author:
        notify.send(
            sender=comment.author,
            recipient=post.author,
            verb='commented',
            action_object=comment,
            target=post,
            description=f'прокомментировал вашу запись "{post.title}"',
            action_url=f'/posts/{post.id}/#comment-{comment.id}',
        )
    
    # Уведомить упомянутых пользователей
    mentioned_users = comment.extract_mentions()
    if mentioned_users:
        notify.send(
            sender=comment.author,
            recipient=mentioned_users,
            verb='mentioned',
            action_object=comment,
            target=post,
            description=f'упомянул вас в комментарии',
            action_url=f'/posts/{post.id}/#comment-{comment.id}',
        )

Уведомления о лайках

@receiver(post_save, sender=Like)
def notify_like(sender, instance, created, **kwargs):
    if not created:
        return
    
    like = instance
    post = like.post
    
    if like.user != post.author:
        notify.send(
            sender=like.user,
            recipient=post.author,
            verb='liked',
            action_object=like,
            target=post,
            description=f'понравилась ваша запись "{post.title}"',
            action_url=f'/posts/{post.id}/',
            data={'like_id': like.id},
        )

Системные уведомления

from django.contrib.auth import get_user_model

User = get_user_model()

def notify_approval(request):
    """Уведомление об одобрении заявки"""
    notify.send(
        sender=None,  # Системное уведомление
        recipient=request.author,
        verb='request_approved',
        action_object=request,
        description=f'Ваша заявка #{request.id} одобрена',
        action_url=f'/requests/{request.id}/',
        data={
            'request_type': request.type,
            'approved_by': request.approved_by.get_full_name(),
            'approved_at': request.approved_at.isoformat(),
        },
    )

Уведомления о событиях календаря

def notify_event_reminder(event, minutes_before=15):
    """Напоминание о событии"""
    from django.utils import timezone
    
    participants = event.participants.all()
    
    notify.send(
        sender=None,
        recipient=list(participants),
        verb='event_reminder',
        action_object=event,
        description=f'Событие "{event.title}" начнется через {minutes_before} минут',
        action_url=f'/calendar/events/{event.id}/',
        data={
            'event_start': event.start_time.isoformat(),
            'location': event.location,
            'minutes_before': minutes_before,
        },
    )

⚙️ Настройка каналов

Создание настроек для пользователя

Настройки создаются автоматически при первом уведомлении.

Можно создать вручную:

from notifications.models_new import UserChannelPreferences

prefs, created = UserChannelPreferences.objects.get_or_create(
    user=user,
    defaults={
        'web_enabled': True,
        'email_enabled': True,
        'push_enabled': True,
        'email_frequency': 'instant',
    }
)

Изменение настроек

# Получить настройки
prefs = user.channel_preferences

# Изменить каналы
prefs.email_enabled = True
prefs.push_enabled = True
prefs.save()

# Отключить определенный тип
prefs.disable_verb('liked')
prefs.disable_verb('followed')

# Включить обратно
prefs.enable_verb('liked')

# Установить тихий режим
prefs.dnd_enabled = True
prefs.dnd_start_time = '22:00'
prefs.dnd_end_time = '08:00'
prefs.save()

Email дайджесты

Для отправки дайджестов используйте Celery task:

# tasks.py
from celery import shared_task
from notifications.channels import send_email_digest
from notifications.models_new import UserChannelPreferences

@shared_task
def send_daily_digests():
    """Отправка ежедневных дайджестов"""
    users = UserChannelPreferences.objects.filter(
        email_enabled=True,
        email_frequency='daily'
    ).select_related('user')
    
    for prefs in users:
        send_email_digest(prefs.user, frequency='daily')

@shared_task
def send_weekly_digests():
    """Отправка еженедельных дайджестов"""
    users = UserChannelPreferences.objects.filter(
        email_enabled=True,
        email_frequency='weekly'
    ).select_related('user')
    
    for prefs in users:
        send_email_digest(prefs.user, frequency='weekly')

Настройте в Celery beat:

# settings.py
CELERY_BEAT_SCHEDULE = {
    'send-daily-digests': {
        'task': 'notifications.tasks.send_daily_digests',
        'schedule': crontab(hour=8, minute=0),  # Каждый день в 8:00
    },
    'send-weekly-digests': {
        'task': 'notifications.tasks.send_weekly_digests',
        'schedule': crontab(day_of_week=1, hour=8, minute=0),  # Понедельник 8:00
    },
}

🔄 Миграция со старой системы

Этап 1: Перенос данных

Создайте management command для миграции:

# management/commands/migrate_notifications.py
from django.core.management.base import BaseCommand
from notifications.models import Notification as OldNotification
from notifications.models_new import Notification as NewNotification

class Command(BaseCommand):
    def handle(self, *args, **options):
        old_notifications = OldNotification.objects.all()
        
        for old in old_notifications:
            NewNotification.objects.create(
                recipient=old.recipient,
                verb=old.notification_type.code,  # type code → verb
                description=old.message,
                action_url=old.action_url,
                unread=not old.is_read,
                timestamp=old.created_at,
                deleted=old.is_archived,
                emailed=old.sent_email,
                data={
                    'old_id': old.id,
                    'old_type': old.notification_type.name,
                },
            )
        
        self.stdout.write(f'Migrated {old_notifications.count()} notifications')

Этап 2: Перенос настроек

# management/commands/migrate_user_settings.py
from django.core.management.base import BaseCommand
from notifications.models import UserNotificationSettings as OldSettings
from notifications.models_new import UserChannelPreferences

class Command(BaseCommand):
    def handle(self, *args, **options):
        # Группируем старые настройки по пользователям
        from django.contrib.auth import get_user_model
        User = get_user_model()
        
        for user in User.objects.all():
            old_settings = OldSettings.objects.filter(user=user)
            
            # Определяем какие каналы включены
            web_enabled = old_settings.filter(send_web=True).exists()
            email_enabled = old_settings.filter(send_email=True).exists()
            telegram_enabled = old_settings.filter(send_telegram=True).exists()
            
            # Частота email
            email_freq = old_settings.filter(
                send_email=True
            ).first()
            email_frequency = email_freq.email_frequency if email_freq else 'instant'
            
            # Создаем новые настройки
            UserChannelPreferences.objects.get_or_create(
                user=user,
                defaults={
                    'web_enabled': web_enabled,
                    'email_enabled': email_enabled,
                    'telegram_enabled': telegram_enabled,
                    'email_frequency': email_frequency,
                }
            )
        
        self.stdout.write('User settings migrated')

Этап 3: Постепенная замена кода

  1. Создание уведомлений - заменить:

    # Старое
    NotificationService.create_notification(
        recipient=user,
        notification_type_code='chat_new_message',
        title='Новое сообщение',
        message='...',
    )
    
    # Новое
    notify.send(
        sender=author,
        recipient=user,
        verb='chat_new_message',
        description='...',
    )
    
  2. Получение уведомлений - заменить:

    # Старое
    Notification.objects.filter(recipient=user, is_read=False)
    
    # Новое
    user.notifications.unread()
    
  3. Отметка прочитанными - заменить:

    # Старое
    NotificationService.mark_as_read(notification_id)
    
    # Новое
    notification.mark_as_read()
    

📖 Рекомендуемые verb типы

Стандартные типы для consistency:

Социальные действия:

  • liked - лайк
  • commented - комментарий
  • shared - репост
  • followed - подписка
  • mentioned - упоминание
  • replied - ответ

Документы:

  • document_created - создан документ
  • document_updated - обновлен документ
  • document_approved - одобрен документ
  • document_rejected - отклонен документ
  • document_signed - подписан документ

Заявки:

  • request_created - создана заявка
  • request_approved - одобрена заявка
  • request_rejected - отклонена заявка
  • request_completed - выполнена заявка

Календарь:

  • event_created - создано событие
  • event_updated - обновлено событие
  • event_reminder - напоминание о событии
  • event_cancelled - отменено событие

Система:

  • system_message - системное сообщение
  • reminder - напоминание
  • alert - предупреждение
  • error - ошибка

🎨 Кастомизация

Добавление своего канала

Создайте обработчик в channels.py:

@receiver(post_save, sender='notifications.Notification')
def send_sms_notification(sender, instance, created, **kwargs):
    """Отправка SMS"""
    if not created:
        return
    
    notification = instance
    prefs = notification.recipient.channel_preferences
    
    if not hasattr(prefs, 'sms_enabled') or not prefs.sms_enabled:
        return
    
    # Ваша логика отправки SMS
    send_sms(
        phone=notification.recipient.phone,
        message=notification.description,
    )

Кастомные шаблоны email

Создайте шаблоны в templates/notifications/email/:

templates/
  notifications/
    email/
      default.html         # Общий шаблон
      liked.html           # Для verb='liked'
      commented.html       # Для verb='commented'
      digest.html          # Дайджест

🧪 Тестирование

from django.test import TestCase
from notifications.signals_new import notify
from notifications.models_new import Notification

class NotificationTests(TestCase):
    def test_create_notification(self):
        """Создание уведомления"""
        notification = notify.send(
            sender=self.user1,
            recipient=self.user2,
            verb='liked',
            description='Test'
        )
        
        self.assertIsInstance(notification, Notification)
        self.assertEqual(notification.recipient, self.user2)
        self.assertEqual(notification.verb, 'liked')
        self.assertTrue(notification.unread)
    
    def test_mark_as_read(self):
        """Отметка прочитанным"""
        notification = notify.send(
            sender=self.user1,
            recipient=self.user2,
            verb='test'
        )
        
        notification.mark_as_read()
        
        self.assertFalse(notification.unread)
        self.assertIsNotNone(notification.timestamp_read)

📈 Производительность

Оптимизация запросов

# Используйте select_related для GenericForeignKey
notifications = user.notifications.select_related(
    'actor_content_type',
    'action_object_content_type',
    'target_content_type',
)

# Prefetch для больших списков
from django.db.models import Prefetch

notifications = Notification.objects.filter(
    recipient=user
).prefetch_related(
    Prefetch('actor_content_type'),
)

Индексы

Все необходимые индексы уже созданы в модели:

  • [recipient, -timestamp]
  • [recipient, unread, -timestamp]
  • [verb, -timestamp]
  • [-timestamp]

🆘 Поддержка

При возникновении проблем:

  1. Проверьте логи: logs/notifications.log
  2. Проверьте настройки пользователя в админке
  3. Проверьте что сигналы подключены в apps.py
  4. Убедитесь что Channels работает для WebSocket

Changelog:

  • v2.0 (9 марта 2026) - Полный рефакторинг, упрощение до 2 моделей
  • v1.0 - Старая система (6 моделей, NotificationService)