lizerk 5220a96684 refactor: Модуль notifications полностью самостоятельный и переиспользуемый
КРИТИЧНЫЕ УЛУЧШЕНИЯ:
✅ Убран хардкод 'EUSRR' → универсальное 'My Site'
✅ Созданы email шаблоны внутри модуля (4 файла)
✅ Централизованная конфигурация в config.py
✅ API перемещен в notifications/api/

Новые файлы:
- config.py - централизованные настройки с fallback значениями
- templates/notifications/email/*.html/*.txt - HTML и текстовые шаблоны
- api/__init__.py, api/views.py, api/urls.py - REST API внутри модуля

Изменения в существующих файлах:
- senders/email.py: использует config.site_name() и config.from_email()
- tasks/email.py: rate_limit, max_retries, retry_delay из config
- tasks/push.py: rate_limit, max_retries, retry_delay из config
- tasks/websocket.py: max_retries, retry_delay из config
- api/v1/notifications/urls.py: реэкспорт из notifications.api (обратная совместимость)

Структура шаблонов:
📁 notifications/templates/notifications/email/
  ├── notification.html - красивый HTML для обычных уведомлений
  ├── notification.txt - текстовая версия
  ├── digest.html - HTML дайджест (daily/weekly)
  └── digest.txt - текстовая версия дайджеста

Конфигурация (config.py):
- DEFAULT_CONFIG с fallback значениями для всех параметров
- get_config() - объединяет с settings.NOTIFICATIONS_CONFIG
- Удобные функции: email_rate_limit(), push_max_retries(), site_name()
- Полностью настраиваемо через settings проекта

API внутри модуля:
- notifications/api/views.py - все endpoint'ы
- notifications/api/urls.py - URL patterns
- Относительные импорты (from ..models)
- Обратная совместимость через api/v1/notifications/urls.py

УНИВЕРСАЛЬНОСТЬ:
✅ Нет хардкода проекта
✅ Все шаблоны внутри модуля
✅ API внутри модуля
✅ Конфигурация через settings
✅ Можно скопировать в любой проект Django 5.2+
2026-03-09 17:06:20 +03:00

113 lines
4.5 KiB
Python
Raw 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.

"""
Celery задачи для отправки email уведомлений
"""
from .base import BaseNotificationTask
from notifications import config
class EmailNotificationTask(BaseNotificationTask):
"""
Celery задача для асинхронной отправки email уведомлений.
Особенности:
- Rate limiting: из config (default: 10 email/минуту)
- Retry: из config (default: 3 попытки с интервалом 5 минут)
- Поддержка кастомных тем и получателей
"""
task_name = "notifications.send_email"
max_retries = config.email_max_retries()
retry_delay = config.email_retry_delay()
rate_limit = config.email_rate_limit()
def send_notification(self, notification, **kwargs) -> bool:
"""
Отправляет email через EmailNotificationSender.
Args:
notification: Объект Notification
**kwargs:
- custom_subject: кастомная тема письма
- recipient_email: явный email получателя
Returns:
True если успешно, False иначе
"""
from notifications.senders.email import EmailNotificationSender
sender = EmailNotificationSender()
return sender.send(notification, **kwargs)
class DigestEmailTask(BaseNotificationTask):
"""
Celery задача для отправки email дайджестов.
Особенности:
- Отправка сводки непрочитанных уведомлений
- Поддержка daily/weekly частоты
- Максимум 50 уведомлений в одном дайджесте
"""
task_name = "notifications.send_digest_email"
max_retries = 2
retry_delay = 600 # 10 минут между попытками
rate_limit = '5/h' # Не более 5 дайджестов в час
def execute(self, celery_task, user_id: int, frequency: str = 'daily', **kwargs):
"""Переопределяем execute для работы с user_id вместо notification_id"""
try:
from django.contrib.auth import get_user_model
from notifications.models import Notification
from notifications.senders.email import EmailNotificationSender
from django.utils import timezone
from datetime import timedelta
User = get_user_model()
user = User.objects.get(id=user_id)
# Определяем период для дайджеста
if frequency == 'weekly':
cutoff = timezone.now() - timedelta(days=7)
else: # daily
cutoff = timezone.now() - timedelta(days=1)
# Получаем непрочитанные уведомления за период
notifications = Notification.objects.filter(
recipient=user,
unread=True,
emailed=False, # Еще не отправлены по email
timestamp__gte=cutoff,
).order_by('-timestamp')[:50] # Максимум 50 в дайджесте
if not notifications.exists():
self.logger.info(f"📭 No notifications for digest: user={user_id}, frequency={frequency}")
return False
# Отправляем дайджест
sender = EmailNotificationSender()
success = sender.send_digest(user, list(notifications), frequency=frequency)
if success:
self.logger.info(
f"✅ Digest sent: user={user_id}, frequency={frequency}, "
f"count={notifications.count()}"
)
else:
self.logger.warning(f"⚠️ Digest failed: user={user_id}, frequency={frequency}")
return success
except Exception as exc:
self.logger.exception(f"❌ Digest task error: user={user_id}, frequency={frequency}")
raise celery_task.retry(exc=exc)
def send_notification(self, notification, **kwargs) -> bool:
"""Не используется для дайджестов"""
pass
# Регистрируем задачи в Celery
send_email_notification = EmailNotificationTask.register_task()
send_digest_email = DigestEmailTask.register_task()