refactor: Переписан API v1 и services для новой архитектуры

BREAKING CHANGES:
- API полностью переписан под новую модель (verb вместо category/type)
- NotificationService заменен на stub с NotImplementedError
- Удалена обратная совместимость

API endpoints (обновлены):
- GET /api/v1/notifications/ - новый формат ответа
- GET /api/v1/notifications/verb-types/ - вместо categories
- GET/PUT /api/v1/notifications/preferences/ - вместо settings
- POST /api/v1/notifications/<id>/unread/ - новый endpoint
- DELETE /api/v1/notifications/delete-all-read/ - новый endpoint

Удалены endpoints:
- /api/v1/notifications/categories/ (нет категорий в v2)
- /api/v1/notifications/settings/* (заменено на preferences)
- /api/v1/notifications/telegram/* (Telegram отключен)
- /api/v1/notifications/push/vapid-key/ (не нужен)
- /api/v1/notifications/push/subscriptions/ (не нужен)

services.py:
- Заменен на stub с NotImplementedError
- Все методы выбрасывают исключения с подсказками миграции
- Документация как мигрировать со старого API

Новая структура ответа API:
- verb вместо category/type
- actor (GenericForeignKey) вместо sender
- description вместо message/title
- timesince() вместо created_at
- unread вместо is_read

Старые файлы:
- Удалены все *.old файлы
This commit is contained in:
lizerk 2026-03-09 16:40:59 +03:00
parent d5b4ae9f43
commit aa614bb4c1
3 changed files with 355 additions and 647 deletions

View File

@ -1,5 +1,7 @@
"""
URL routing для API v1 уведомлений.
URL routing для API v2 уведомлений.
Новая архитектура: verb-based notifications с channel preferences.
"""
from django.urls import path
from . import views
@ -7,29 +9,24 @@ from . import views
app_name = 'notifications_api_v1'
urlpatterns = [
# CRUD уведомлений
# Список и счетчики
path('', views.get_notifications, name='list'),
path('count/', views.get_unread_count, name='count'),
path('verb-types/', views.get_verb_types, name='verb_types'),
# Операции с уведомлениями
path('<int:notification_id>/read/', views.mark_as_read, name='mark_read'),
path('read-all/', views.mark_all_as_read, name='mark_all_read'),
path('<int:notification_id>/unread/', views.mark_as_unread, name='mark_unread'),
path('<int:notification_id>/', views.delete_notification, name='delete'),
# Категории
path('categories/', views.get_categories, name='categories'),
# Массовые операции
path('read-all/', views.mark_all_as_read, name='mark_all_read'),
path('delete-all-read/', views.delete_all_read, name='delete_all_read'),
# Настройки
path('settings/', views.get_user_settings, name='settings_get'),
path('settings/update/', views.update_user_settings, name='settings_update'),
path('settings/category/update/', views.update_category_settings, name='category_settings_update'),
# Telegram интеграция
path('telegram/status/', views.get_telegram_link_status, name='telegram_status'),
path('telegram/generate-code/', views.generate_telegram_link_code, name='telegram_generate_code'),
path('telegram/unlink/', views.unlink_telegram, name='telegram_unlink'),
# Настройки каналов
path('preferences/', views.channel_preferences, name='preferences'),
# Web Push интеграция
path('push/vapid-key/', views.get_vapid_public_key, name='push_vapid_key'),
path('push/subscribe/', views.subscribe_push, name='push_subscribe'),
path('push/unsubscribe/', views.unsubscribe_push, name='push_unsubscribe'),
path('push/subscriptions/', views.get_push_subscriptions, name='push_subscriptions'),
]

View File

@ -1,79 +1,84 @@
"""
API endpoints для работы с уведомлениями (версия 1).
API endpoints для системы уведомлений v2.0
Новая архитектура:
- Notification с GenericForeignKey (actor/action_object/target)
- verb вместо category/type
- UserChannelPreferences для настроек каналов
"""
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from notifications.models import Notification, NotificationCategory, NotificationType
from notifications.services import NotificationService
from django.db import models
from notifications.models import Notification, UserChannelPreferences
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_notifications(request):
"""Получить список уведомлений с пагинацией и фильтрами"""
"""
Получить список уведомлений с пагинацией и фильтрами
Query params:
- page: номер страницы (default: 1)
- page_size: размер страницы (default: 20)
- verb: фильтр по типу действия (liked, commented, mentioned и т.д.)
- unread_only: только непрочитанные (true/false)
- search: поиск по описанию
"""
page = int(request.GET.get('page', 1))
page_size = int(request.GET.get('page_size', 20))
category = request.GET.get('category')
is_read = request.GET.get('is_read')
search = request.GET.get('search', '').strip()
verb = request.GET.get('verb')
unread_only = request.GET.get('unread_only', 'false').lower() == 'true'
search = request.GET.get('search', '').strip()
# Базовый queryset
queryset = Notification.objects.filter(
recipient=request.user,
is_archived=False
).select_related(
'notification_type',
'notification_type__category'
).order_by('-created_at')
deleted=False,
).order_by('-timestamp')
# Фильтр по категории
if category:
queryset = queryset.filter(notification_type__category__code=category)
# Фильтр по статусу прочитанности
if is_read and is_read in ('true', 'false'):
queryset = queryset.filter(is_read=(is_read == 'true'))
elif unread_only:
queryset = queryset.filter(is_read=False)
# Поиск по заголовку и тексту
# Фильтры
if verb:
queryset = queryset.filter(verb=verb)
if unread_only:
queryset = queryset.unread()
if search:
from django.db.models import Q
queryset = queryset.filter(
Q(title__icontains=search) |
Q(message__icontains=search) |
Q(short_message__icontains=search)
)
queryset = queryset.filter(description__icontains=search)
# Пагинация
total = queryset.count()
start = (page - 1) * page_size
end = start + page_size
notifications = queryset[start:end]
# Сериализация
data = {
'total': total,
'page': page,
'page_size': page_size,
'unread_count': queryset.filter(unread=True).count(),
'notifications': [
{
'id': n.id,
'title': n.title,
'message': n.message,
'short_message': n.short_message or n.message[:150], # Краткое сообщение для превью
'category': n.notification_type.category.code,
'category_name': n.notification_type.category.name,
'icon': n.notification_type.category.icon,
'color': n.notification_type.category.color,
'priority': n.notification_type.priority,
'is_read': n.is_read,
'verb': n.verb,
'description': n.description,
'actor': {
'id': n.actor.id if n.actor else None,
'name': str(n.actor) if n.actor else 'Система',
'type': n.actor_content_type.model if n.actor else None,
} if n.actor else None,
'action_url': n.action_url,
'action_text': n.action_text,
'created_at': n.created_at.isoformat(),
'unread': n.unread,
'public': n.public,
'deleted': n.deleted,
'emailed': n.emailed,
'timestamp': n.timestamp.isoformat(),
'timesince': n.timesince(),
}
for n in notifications
]
@ -88,8 +93,8 @@ def get_unread_count(request):
"""Получить количество непрочитанных уведомлений"""
count = Notification.objects.filter(
recipient=request.user,
is_read=False,
is_archived=False
unread=True,
deleted=False,
).count()
return Response({'count': count})
@ -99,11 +104,42 @@ def get_unread_count(request):
@permission_classes([IsAuthenticated])
def mark_as_read(request, notification_id):
"""Отметить уведомление как прочитанное"""
success = NotificationService.mark_as_read(notification_id, request.user)
try:
notification = Notification.objects.get(
id=notification_id,
recipient=request.user
)
notification.mark_as_read()
return Response({
'status': 'success',
'notification_id': notification_id
})
except Notification.DoesNotExist:
return Response(
{'status': 'error', 'message': 'Notification not found'},
status=status.HTTP_404_NOT_FOUND
)
if success:
return Response({'status': 'success'})
else:
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def mark_as_unread(request, notification_id):
"""Отметить уведомление как непрочитанное"""
try:
notification = Notification.objects.get(
id=notification_id,
recipient=request.user
)
notification.mark_as_unread()
return Response({
'status': 'success',
'notification_id': notification_id
})
except Notification.DoesNotExist:
return Response(
{'status': 'error', 'message': 'Notification not found'},
status=status.HTTP_404_NOT_FOUND
@ -114,23 +150,39 @@ def mark_as_read(request, notification_id):
@permission_classes([IsAuthenticated])
def mark_all_as_read(request):
"""Отметить все уведомления как прочитанные"""
category = request.data.get('category')
count = NotificationService.mark_all_as_read(request.user, category)
verb = request.data.get('verb') # Опционально - только определенный тип
queryset = Notification.objects.filter(
recipient=request.user,
unread=True,
deleted=False,
)
if verb:
queryset = queryset.filter(verb=verb)
count = queryset.mark_all_as_read()
return Response({'status': 'success', 'count': count})
return Response({
'status': 'success',
'count': count
})
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def delete_notification(request, notification_id):
"""Удалить уведомление (архивировать)"""
"""Удалить уведомление (soft delete)"""
try:
notification = Notification.objects.get(
id=notification_id,
recipient=request.user
)
notification.archive()
notification.deleted = True
notification.save(update_fields=['deleted'])
return Response({'status': 'success'})
except Notification.DoesNotExist:
return Response(
{'status': 'error', 'message': 'Notification not found'},
@ -138,438 +190,206 @@ def delete_notification(request, notification_id):
)
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def delete_all_read(request):
"""Удалить все прочитанные уведомления"""
count = Notification.objects.filter(
recipient=request.user,
unread=False,
deleted=False,
).update(deleted=True)
return Response({
'status': 'success',
'count': count
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_categories(request):
"""Получить список категорий уведомлений"""
categories = NotificationCategory.objects.filter(is_active=True)
def get_verb_types(request):
"""
Получить список типов (verb) уведомлений с количеством
Возвращает статистику по типам уведомлений пользователя.
"""
from django.db.models import Count, Q
verb_stats = Notification.objects.filter(
recipient=request.user,
deleted=False,
).values('verb').annotate(
total=Count('id'),
unread=Count('id', filter=Q(unread=True))
).order_by('-total')
# Маппинг verb → человекочитаемое название
verb_names = {
'liked': 'Понравилось',
'commented': 'Прокомментировал',
'mentioned': 'Упомянул',
'messaged': 'Отправил сообщение',
'shared': 'Поделился',
'posted': 'Опубликовал',
'created': 'Создал',
'updated': 'Обновил',
'approved': 'Одобрил',
'rejected': 'Отклонил',
'scheduled': 'Запланировал',
'notified': 'Уведомил',
}
data = [
{
'code': cat.code,
'name': cat.name,
'icon': cat.icon,
'color': cat.color,
'verb': item['verb'],
'name': verb_names.get(item['verb'], item['verb'].title()),
'total': item['total'],
'unread': item['unread'],
}
for cat in categories
for item in verb_stats
]
return Response(data)
return Response({'verb_types': data})
@api_view(['GET'])
@api_view(['GET', 'PUT'])
@permission_classes([IsAuthenticated])
def get_user_settings(request):
"""Получить настройки уведомлений пользователя по категориям"""
categories = NotificationCategory.objects.filter(is_active=True)
def channel_preferences(request):
"""
Получить или обновить настройки каналов уведомлений
settings_by_category = {}
GET: Возвращает текущие настройки
PUT: Обновляет настройки
for category in categories:
# Получить типы уведомлений этой категории
types = NotificationType.objects.filter(
category=category,
is_active=True
)
if not types.exists():
continue
# Получить настройки первого типа категории как представитель
# (предполагается, что настройки одинаковы для всей категории)
first_type = types.first()
settings = NotificationService.get_user_settings(
request.user, first_type
)
settings_by_category[category.code] = {
'is_enabled': settings.is_enabled,
'web_enabled': settings.send_web,
'email_enabled': settings.send_email,
'email_frequency': settings.email_frequency,
'telegram_enabled': settings.send_telegram,
Настройки:
- web_enabled: WebSocket уведомления (bool)
- email_enabled: Email уведомления (bool)
- email_frequency: instant/daily/weekly/disabled
- push_enabled: Web Push уведомления (bool)
- dnd_enabled: Режим "Не беспокоить" (bool)
- dnd_start_time: Начало DND (HH:MM)
- dnd_end_time: Конец DND (HH:MM)
- disabled_verbs: Список отключенных типов (array)
"""
# Получаем или создаем настройки
prefs, created = UserChannelPreferences.objects.get_or_create(
user=request.user
)
if request.method == 'GET':
data = {
'web_enabled': prefs.web_enabled,
'email_enabled': prefs.email_enabled,
'email_frequency': prefs.email_frequency,
'push_enabled': prefs.push_enabled,
'dnd_enabled': prefs.dnd_enabled,
'dnd_start_time': prefs.dnd_start_time.strftime('%H:%M') if prefs.dnd_start_time else None,
'dnd_end_time': prefs.dnd_end_time.strftime('%H:%M') if prefs.dnd_end_time else None,
'disabled_verbs': prefs.disabled_verbs,
}
return Response(data)
return Response({
'settings': settings_by_category
})
@api_view(['PUT'])
@permission_classes([IsAuthenticated])
def update_user_settings(request):
"""Обновить настройки уведомлений пользователя"""
notification_type_code = request.data.get('notification_type_code')
try:
notification_type = NotificationType.objects.get(
code=notification_type_code
)
except NotificationType.DoesNotExist:
return Response(
{'status': 'error', 'message': 'Notification type not found'},
status=status.HTTP_404_NOT_FOUND
)
settings = NotificationService.get_user_settings(
request.user, notification_type
)
# Обновить настройки
if 'is_enabled' in request.data and not notification_type.is_required:
settings.is_enabled = request.data['is_enabled']
if 'send_web' in request.data:
settings.send_web = request.data['send_web']
if 'send_email' in request.data:
settings.send_email = request.data['send_email']
if 'send_telegram' in request.data:
settings.send_telegram = request.data['send_telegram']
if 'send_whatsapp' in request.data:
settings.send_whatsapp = request.data['send_whatsapp']
if 'send_wechat' in request.data:
settings.send_wechat = request.data['send_wechat']
if 'quiet_hours_enabled' in request.data:
settings.quiet_hours_enabled = request.data['quiet_hours_enabled']
if 'quiet_start_time' in request.data:
settings.quiet_start_time = request.data['quiet_start_time']
if 'quiet_end_time' in request.data:
settings.quiet_end_time = request.data['quiet_end_time']
settings.save()
return Response({'status': 'success'})
@api_view(['PUT'])
@permission_classes([IsAuthenticated])
def update_category_settings(request):
"""
Обновить настройки для всех уведомлений категории
"""
category_code = request.data.get('category')
if not category_code:
return Response(
{'status': 'error', 'message': 'Category is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
category = NotificationCategory.objects.get(code=category_code)
except NotificationCategory.DoesNotExist:
return Response(
{'status': 'error', 'message': 'Category not found'},
status=status.HTTP_404_NOT_FOUND
)
# Получить все типы уведомлений категории
notification_types = NotificationType.objects.filter(
category=category,
is_active=True
)
# Обновить настройки для каждого типа
updated_count = 0
for notification_type in notification_types:
settings = NotificationService.get_user_settings(
request.user, notification_type
)
# Обновить поля
if 'is_enabled' in request.data and not notification_type.is_required:
settings.is_enabled = request.data['is_enabled']
elif request.method == 'PUT':
# Обновляем настройки
if 'web_enabled' in request.data:
settings.send_web = request.data['web_enabled']
prefs.web_enabled = request.data['web_enabled']
if 'email_enabled' in request.data:
settings.send_email = request.data['email_enabled']
prefs.email_enabled = request.data['email_enabled']
if 'email_frequency' in request.data:
settings.email_frequency = request.data['email_frequency']
frequency = request.data['email_frequency']
if frequency in ['instant', 'daily', 'weekly', 'disabled']:
prefs.email_frequency = frequency
if 'telegram_enabled' in request.data:
settings.send_telegram = request.data['telegram_enabled']
if 'push_enabled' in request.data:
prefs.push_enabled = request.data['push_enabled']
settings.save()
updated_count += 1
return Response({
'status': 'success',
'updated_count': updated_count
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_telegram_link_status(request):
"""Получить статус привязки Telegram аккаунта"""
from notifications.telegram_models import TelegramUser
try:
tg_user = TelegramUser.objects.get(
user=request.user,
is_active=True,
telegram_id__isnull=False
)
if 'dnd_enabled' in request.data:
prefs.dnd_enabled = request.data['dnd_enabled']
return Response({
'is_linked': True,
'telegram_username': tg_user.telegram_username,
'first_name': tg_user.first_name,
'last_name': tg_user.last_name,
'linked_at': tg_user.linked_at,
'is_blocked': tg_user.is_blocked,
})
except TelegramUser.DoesNotExist:
# Проверяем, может быть есть незавершенная привязка с кодом
pending = TelegramUser.objects.filter(
user=request.user,
link_code__isnull=False
).first()
if 'dnd_start_time' in request.data:
from datetime import datetime
time_str = request.data['dnd_start_time']
if time_str:
prefs.dnd_start_time = datetime.strptime(time_str, '%H:%M').time()
response_data = {'is_linked': False}
if 'dnd_end_time' in request.data:
from datetime import datetime
time_str = request.data['dnd_end_time']
if time_str:
prefs.dnd_end_time = datetime.strptime(time_str, '%H:%M').time()
if pending and pending.is_link_code_valid():
response_data['link_code'] = pending.link_code
if 'disabled_verbs' in request.data:
prefs.disabled_verbs = request.data['disabled_verbs']
return Response(response_data)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def generate_telegram_link_code(request):
"""Сгенерировать код для привязки Telegram аккаунта"""
from notifications.telegram_models import TelegramUser
# Проверяем что аккаунт еще не привязан
existing = TelegramUser.objects.filter(
user=request.user,
is_active=True,
telegram_id__isnull=False
).first()
if existing:
return Response({
'status': 'error',
'message': 'Telegram аккаунт уже привязан'
}, status=status.HTTP_400_BAD_REQUEST)
# Получаем или создаем запись БЕЗ telegram_id (он будет установлен при привязке)
tg_user = TelegramUser.objects.filter(
user=request.user
).first()
if not tg_user:
tg_user = TelegramUser.objects.create(
user=request.user,
is_active=False
)
# Генерируем новый код (даже если уже был старый)
link_code = tg_user.generate_link_code()
from django.conf import settings
bot_username = getattr(settings, 'TELEGRAM_BOT_USERNAME', 'eusrr_bot')
return Response({
'status': 'success',
'link_code': link_code,
'bot_username': bot_username,
'bot_link': f'https://t.me/{bot_username}',
'expires_in_seconds': 900, # 15 минут
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def unlink_telegram(request):
"""Отвязать Telegram аккаунт"""
from notifications.telegram_models import TelegramUser
try:
tg_user = TelegramUser.objects.get(
user=request.user,
is_active=True
)
tg_user.is_active = False
tg_user.save()
prefs.save()
return Response({
'status': 'success',
'message': 'Telegram аккаунт отвязан'
'message': 'Preferences updated'
})
except TelegramUser.DoesNotExist:
return Response({
'status': 'error',
'message': 'Telegram аккаунт не привязан'
}, status=status.HTTP_404_NOT_FOUND)
# ============================================
# Web Push API (для браузерных уведомлений)
# ============================================
from django.conf import settings
from push_notifications.models import WebPushDevice
import logging
logger = logging.getLogger(__name__)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_vapid_public_key(request):
"""
Получить публичный VAPID ключ для подписки на push-уведомления.
Этот ключ нужен браузеру для создания подписки.
"""
return Response({
'vapid_public_key': settings.VAPID_PUBLIC_KEY
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def subscribe_push(request):
"""
Подписаться на Web Push уведомления.
Подписка на Web Push уведомления
Ожидаемые данные:
{
"endpoint": "https://fcm.googleapis.com/...",
"keys": {
"p256dh": "...",
"auth": "..."
},
"user_agent": "...", // опционально
"device_name": "..." // опционально
}
Body:
- subscription: объект subscription от браузера
"""
try:
endpoint = request.data.get('endpoint')
keys = request.data.get('keys', {})
p256dh_key = keys.get('p256dh')
auth_key = keys.get('auth')
user_agent = request.data.get('user_agent', '')
device_name = request.data.get('device_name', '') or 'Unknown'
if not endpoint or not p256dh_key or not auth_key:
return Response({
'status': 'error',
'message': 'Отсутствуют обязательные поля: endpoint, keys.p256dh, keys.auth'
}, status=status.HTTP_400_BAD_REQUEST)
# Обновляем или создаем устройство (django-push-notifications)
device, created = WebPushDevice.objects.update_or_create(
user=request.user,
registration_id=endpoint,
defaults={
'p256dh': p256dh_key,
'auth': auth_key,
'browser': device_name,
'active': True,
}
from push_notifications.models import WebPushDevice
subscription_data = request.data.get('subscription')
if not subscription_data:
return Response(
{'status': 'error', 'message': 'Subscription data required'},
status=status.HTTP_400_BAD_REQUEST
)
# Логируем только создание нового устройства, не обновление
if created:
logger.info(f"[WebPush] New device for user {request.user.id}")
return Response({
'status': 'success',
'message': 'Подписка на push-уведомления активирована',
'created': created
})
except Exception as e:
logger.exception(f"[WebPush] Error subscribing user {request.user.id}: {e}")
return Response({
'status': 'error',
'message': 'Ошибка при создании подписки'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def unsubscribe_push(request):
"""
Отписаться от Web Push уведомлений.
Ожидаемые данные:
{
"endpoint": "https://fcm.googleapis.com/..."
}
Если endpoint не указан - отписывает от всех подписок пользователя.
"""
try:
endpoint = request.data.get('endpoint')
if endpoint:
# Удаляем конкретное устройство
deleted, _ = WebPushDevice.objects.filter(
user=request.user,
registration_id=endpoint
).delete()
if deleted:
logger.info(f"[WebPush] Deleted device for user {request.user.id} (endpoint: {endpoint[:50]}...)")
return Response({
'status': 'success',
'message': 'Подписка удалена'
})
else:
return Response({
'status': 'error',
'message': 'Подписка не найдена'
}, status=status.HTTP_404_NOT_FOUND)
else:
# Удаляем все устройства пользователя
deleted, _ = WebPushDevice.objects.filter(user=request.user).delete()
logger.info(f"[WebPush] Deleted all ({deleted}) devices for user {request.user.id}")
return Response({
'status': 'success',
'message': f'Удалено подписок: {deleted}'
})
except Exception as e:
logger.exception(f"[WebPush] Error unsubscribing user {request.user.id}: {e}")
return Response({
'status': 'error',
'message': 'Ошибка при удалении подписки'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_push_subscriptions(request):
"""
Получить список активных push-подписок пользователя.
"""
devices = WebPushDevice.objects.filter(
# Регистрируем устройство
device, created = WebPushDevice.objects.update_or_create(
user=request.user,
active=True
).order_by('-date_created')
registration_id=subscription_data.get('endpoint'),
defaults={
'p256dh': subscription_data.get('keys', {}).get('p256dh'),
'auth': subscription_data.get('keys', {}).get('auth'),
'browser': request.data.get('browser', 'unknown'),
'active': True,
}
)
return Response({
'subscriptions': [
{
'id': device.id,
'device_name': device.browser or 'Неизвестное устройство',
'user_agent': '',
'created_at': device.date_created.isoformat(),
'updated_at': device.date_created.isoformat(),
}
for device in devices
]
'status': 'success',
'created': created,
'device_id': device.id
})
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def unsubscribe_push(request):
"""Отписка от Web Push уведомлений"""
from push_notifications.models import WebPushDevice
endpoint = request.data.get('endpoint')
if endpoint:
deleted_count, _ = WebPushDevice.objects.filter(
user=request.user,
registration_id=endpoint
).delete()
else:
# Удалить все устройства пользователя
deleted_count, _ = WebPushDevice.objects.filter(
user=request.user
).delete()
return Response({
'status': 'success',
'deleted_count': deleted_count
})

View File

@ -1,199 +1,90 @@
"""
Слой обратной совместимости для старого API уведомлений.
DEPRECATED: Этот модуль удален в v2.0
DEPRECATED: Этот модуль предоставляет facade для старого кода.
Новый код должен использовать напрямую:
- from notifications.signals import notify
- notify.send(sender=user, recipient=other, verb='liked', ...)
Вместо NotificationService используйте:
1. Создание уведомлений:
СТАРОЕ:
NotificationService.create_notification(
recipient=user,
notification_type_code='message',
title='Заголовок',
message='Текст',
action_url='/link/'
)
НОВОЕ:
from notifications.signals import notify
notify.send(
sender=actor,
recipient=user,
verb='messaged',
description='Текст',
action_url='/link/'
)
2. Отметка как прочитанное:
СТАРОЕ:
NotificationService.mark_as_read(notification_id, user)
НОВОЕ:
notification = Notification.objects.get(id=notification_id)
notification.mark_as_read()
3. Настройки:
СТАРОЕ:
settings = NotificationService.get_user_settings(user)
НОВОЕ:
prefs = UserChannelPreferences.objects.get(user=user)
# или через related name:
prefs = user.channel_preferences
Документация: backend/notifications/README.md
"""
import logging
from typing import Optional, Dict, Any
from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
User = get_user_model()
# Для обратной совместимости можно оставить минимальный stub
# который будет выбрасывать ошибки с подсказками как мигрировать
class NotificationService:
"""
Facade для обратной совместимости со старым API.
Все методы теперь работают через новую систему (notify.send()).
"""
"""DEPRECATED: Удален в v2.0. См. docstring модуля."""
@staticmethod
def create_notification(
recipient: User,
notification_type_code: str,
title: str,
message: str,
content_object=None,
action_url: str = '',
action_text: str = 'Посмотреть',
metadata: Optional[Dict[str, Any]] = None,
send_immediately: bool = True,
):
"""
Создает уведомление (старый API → новая система).
DEPRECATED: Используйте notify.send() напрямую.
Args:
recipient: Получатель уведомления
notification_type_code: Код типа (mapping в verb)
title: Заголовок (не используется в новой системе)
message: Описание уведомления
content_object: Объект-источник (→ action_object)
action_url: URL для действия
action_text: Текст кнопки (не используется)
metadata: Дополнительные данные
send_immediately: Игнорируется (всегда через Celery)
Returns:
Notification объект (новая модель)
"""
from .signals import notify
# Маппинг старых типов на новые verbs
type_to_verb = {
'message': 'messaged',
'message_new': 'messaged',
'document': 'shared',
'document_new': 'shared',
'request': 'created',
'request_status_changed': 'updated',
'request_comment_added': 'commented',
'feed_post': 'posted',
'feed_comment': 'commented',
'feed_like': 'liked',
'calendar_event': 'scheduled',
'procurement': 'created',
'procurement_approved': 'approved',
'procurement_rejected': 'rejected',
}
verb = type_to_verb.get(notification_type_code, 'notified')
# Создаем через новую систему
notification = notify.send(
sender=None, # System notification
recipient=recipient,
verb=verb,
action_object=content_object,
description=message,
action_url=action_url,
)
# notify.send() возвращает список [(signal, [notifications])]
if notification and len(notification) > 0:
return notification[0][1][0] if notification[0][1] else None
return None
@staticmethod
def create_notification_async(
recipient: User,
notification_type_code: str,
title: str,
message: str,
content_object=None,
action_url: str = '',
action_text: str = 'Посмотреть',
metadata: Optional[Dict[str, Any]] = None,
):
"""
Асинхронное создание уведомления.
DEPRECATED: Новая система всегда асинхронна через Celery.
Этот метод теперь идентичен create_notification().
"""
return NotificationService.create_notification(
recipient=recipient,
notification_type_code=notification_type_code,
title=title,
message=message,
content_object=content_object,
action_url=action_url,
action_text=action_text,
metadata=metadata,
send_immediately=True,
def create_notification(*args, **kwargs):
raise NotImplementedError(
"NotificationService.create_notification() удален. "
"Используйте: notify.send(sender=actor, recipient=user, verb='...', description='...')"
)
@staticmethod
def mark_as_read(notification_id: int, user: User) -> bool:
"""
Отмечает уведомление как прочитанное.
Args:
notification_id: ID уведомления
user: Пользователь (проверка владения)
Returns:
True если успешно, False иначе
"""
try:
from .models import Notification
notification = Notification.objects.get(
id=notification_id,
recipient=user
)
notification.mark_as_read()
return True
except Notification.DoesNotExist:
logger.warning(f"Notification {notification_id} not found for user {user.id}")
return False
@staticmethod
def mark_all_as_read(user: User, category: Optional[str] = None) -> int:
"""
Отмечает все уведомления как прочитанные.
Args:
user: Пользователь
category: Категория (не используется в новой системе)
Returns:
Количество отмеченных уведомлений
"""
from .models import Notification
return Notification.objects.filter(
recipient=user,
unread=True
).mark_all_as_read()
@staticmethod
def get_user_settings(user: User, setting_type: Optional[str] = None):
"""
Получает настройки уведомлений пользователя.
DEPRECATED: Используйте UserChannelPreferences напрямую.
Args:
user: Пользователь
setting_type: Тип настройки (игнорируется)
Returns:
UserChannelPreferences объект
"""
try:
return user.channel_preferences
except Exception:
from .models import UserChannelPreferences
return UserChannelPreferences.objects.create(user=user)
@staticmethod
def send_websocket_notification(user_id: int, notification_data: Optional[dict] = None):
"""
Отправка WebSocket уведомления.
DEPRECATED: WebSocket уведомления отправляются автоматически
через channels.py при создании Notification.
"""
logger.warning(
"NotificationService.send_websocket_notification() is deprecated. "
"WebSocket notifications are sent automatically via channels.py"
def create_notification_async(*args, **kwargs):
raise NotImplementedError(
"NotificationService.create_notification_async() удален. "
"Все уведомления теперь асинхронны через Celery. "
"Используйте: notify.send(...)"
)
return True
@staticmethod
def mark_as_read(*args, **kwargs):
raise NotImplementedError(
"NotificationService.mark_as_read() удален. "
"Используйте: notification.mark_as_read()"
)
@staticmethod
def mark_all_as_read(*args, **kwargs):
raise NotImplementedError(
"NotificationService.mark_all_as_read() удален. "
"Используйте: Notification.objects.filter(...).mark_all_as_read()"
)
@staticmethod
def get_user_settings(*args, **kwargs):
raise NotImplementedError(
"NotificationService.get_user_settings() удален. "
"Используйте: user.channel_preferences или UserChannelPreferences.objects.get(user=user)"
)