- Убрана поддержка Telegram из каналов доставки - Удалено telegram_enabled из UserChannelPreferences - Обновлена документация - Сфокусировались на WebSocket, Email и Web Push Фокус: веб-уведомления, email дайджесты и web push
23 KiB
Универсальная система уведомлений для Django
Версия: 2.0
Дата: 9 марта 2026
Основана на: django-notifications-hq архитектуре
Совместимость: Django 5.2+
📋 Оглавление
- Обзор
- Архитектура
- Установка
- Быстрый старт
- API Reference
- Примеры использования
- Настройка каналов
- Миграция со старой системы
🎯 Обзор
Упрощенная универсальная система уведомлений, которая:
✅ Проста - 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/weeklydnd_enabled- режим "Не беспокоить" (bool)dnd_start_time/dnd_end_time- время DNDdisabled_verbs- список отключенных типов (JSON array)
Компоненты
- signals_new.py -
notify.send()API для создания уведомлений - channels.py - роутинг по каналам доставки (WebSocket, Email, Push)
- models_new.py - модели с QuerySet методами
- admin_new.py - Django Admin интерфейс
📦 Установка
Шаг 1: Применить миграции
python manage.py makemigrations notifications
python manage.py migrate notifications
Шаг 2: Обновить код проекта
Код уже готов к использованию! Файлы:
backend/notifications/models_new.pybackend/notifications/signals_new.pybackend/notifications/channels.pybackend/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: Постепенная замена кода
-
Создание уведомлений - заменить:
# Старое NotificationService.create_notification( recipient=user, notification_type_code='chat_new_message', title='Новое сообщение', message='...', ) # Новое notify.send( sender=author, recipient=user, verb='chat_new_message', description='...', ) -
Получение уведомлений - заменить:
# Старое Notification.objects.filter(recipient=user, is_read=False) # Новое user.notifications.unread() -
Отметка прочитанными - заменить:
# Старое 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]
🆘 Поддержка
При возникновении проблем:
- Проверьте логи:
logs/notifications.log - Проверьте настройки пользователя в админке
- Проверьте что сигналы подключены в
apps.py - Убедитесь что Channels работает для WebSocket
Changelog:
- v2.0 (9 марта 2026) - Полный рефакторинг, упрощение до 2 моделей
- v1.0 - Старая система (6 моделей, NotificationService)