Проблема:
- Service Worker файл (sw.js) находился в корне backend/
- Hardcoded пути к иконкам в sw.js и push.py (/static/img/logo-192.png)
- Функция serve_service_worker() в главном urls.py проекта
- Зависимость от project-specific структуры
- Невозможность переиспользовать приложение без изменений
Решение:
Service Worker на месте:
- backend/notifications/static/notifications/sw.js
- backend/notifications/views.py - serve_service_worker()
- backend/notifications/urls.py - добавлен endpoint sw.js
- backend/eusrr_backend/urls.py - импорт view из notifications
Удаление hardcoded путей:
- sw.js: убрана константа icon='/static/img/logo-192.png'
- senders/push.py: убран hardcoded 'icon': '/static/img/logo.png'
- config.py: добавлены PUSH_DEFAULT_ICON, PUSH_DEFAULT_BADGE (None = browser default)
- README.md: документация по настройке через NOTIFICATIONS_CONFIG
Изменения:
- Создан: notifications/views.py с serve_service_worker()
- Создан: notifications/static/notifications/sw.js
- Изменен: notifications/urls.py - добавлен path('sw.js')
- Изменен: notifications/config.py - настройки иконок
- Изменен: notifications/senders/push.py - использование config
- Изменен: notifications/README.md - документация
- Упрощен: eusrr_backend/urls.py - удалена функция, добавлен импорт
- Удалены: backend/sw.js, backend/static/sw.js, backend/communications/sw.js
Преимущества:
- ✅ Полностью автономное приложение
- ✅ Нет зависимостей от структуры проекта
- ✅ Конфигурируемые иконки через settings.py
- ✅ Browser defaults если иконки не указаны
- ✅ Легко переносить между проектами
- ✅ Единообразие архитектуры (SRP)
Верификация:
- python manage.py check - успешно
115 lines
4.7 KiB
Python
115 lines
4.7 KiB
Python
"""
|
|
Web Push отправитель для browser push notifications
|
|
"""
|
|
from push_notifications.models import WebPushDevice
|
|
|
|
from .base import BaseNotificationSender
|
|
from ..config import get
|
|
|
|
|
|
class PushNotificationSender(BaseNotificationSender):
|
|
"""
|
|
Отправитель Web Push уведомлений через django-push-notifications.
|
|
Доставляет уведомления в браузер даже когда вкладка закрыта.
|
|
"""
|
|
|
|
def can_send(self, notification, user_preferences) -> bool:
|
|
"""Проверяет, включены ли push уведомления"""
|
|
if not user_preferences.push_enabled:
|
|
self.log_skip(notification, "push_enabled=False")
|
|
return False
|
|
return True
|
|
|
|
def send(self, notification, **kwargs) -> bool:
|
|
"""
|
|
Отправляет Web Push уведомление.
|
|
|
|
Args:
|
|
notification: Объект Notification
|
|
**kwargs: Дополнительные параметры
|
|
|
|
Returns:
|
|
True если отправлено успешно, False иначе
|
|
"""
|
|
try:
|
|
user = notification.recipient
|
|
|
|
# Получаем активные устройства через django-push-notifications
|
|
devices = WebPushDevice.objects.filter(
|
|
user=user,
|
|
active=True
|
|
)
|
|
|
|
if not devices.exists():
|
|
self.log_skip(notification, "no active push devices")
|
|
return False
|
|
|
|
# Формируем данные для push
|
|
actor_str = str(notification.actor) if notification.actor else 'Система'
|
|
title = f'{actor_str} {notification.verb}'
|
|
body = notification.description or ''
|
|
|
|
# Получаем иконки из конфигурации (None = browser default)
|
|
default_icon = get('PUSH_DEFAULT_ICON')
|
|
default_badge = get('PUSH_DEFAULT_BADGE')
|
|
|
|
# django-push-notifications использует другой формат
|
|
message = {
|
|
'head': title,
|
|
'body': body,
|
|
'url': notification.action_url or '/',
|
|
'tag': f'notification-{notification.id}',
|
|
'requireInteraction': False,
|
|
'data': {
|
|
'notification_id': notification.id,
|
|
'verb': notification.verb,
|
|
}
|
|
}
|
|
|
|
# Добавляем иконки только если они настроены
|
|
if default_icon:
|
|
message['icon'] = default_icon
|
|
if default_badge:
|
|
message['badge'] = default_badge
|
|
|
|
sent_count = 0
|
|
failed_count = 0
|
|
|
|
# Отправляем через все устройства пользователя
|
|
for device in devices:
|
|
try:
|
|
device.send_message(message)
|
|
sent_count += 1
|
|
self.logger.debug(
|
|
f"Push отправлен на device {device.id} ({device.browser})"
|
|
)
|
|
except Exception as e:
|
|
failed_count += 1
|
|
self.logger.error(
|
|
f"Ошибка отправки push на device {device.id}: {e}"
|
|
)
|
|
# Деактивируем устройство если ошибка
|
|
if "expired" in str(e).lower() or "unregistered" in str(e).lower():
|
|
device.active = False
|
|
device.save()
|
|
self.logger.info(f"Device {device.id} деактивирован")
|
|
|
|
if sent_count > 0:
|
|
self.log_success(notification, f"{sent_count} devices")
|
|
if failed_count > 0:
|
|
self.logger.warning(
|
|
f"Push отправлен частично: {sent_count} успешно, "
|
|
f"{failed_count} ошибок"
|
|
)
|
|
return True
|
|
else:
|
|
self.log_skip(notification, f"no successful sends ({failed_count} failures)")
|
|
return False
|
|
|
|
except ImportError:
|
|
self.log_skip(notification, "django-push-notifications not installed")
|
|
return False
|
|
except Exception as e:
|
|
self.log_error(notification, e, f"user_{notification.recipient.id}")
|
|
return False
|