uchill/backend/apps/notifications/views.py

476 lines
19 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.

"""
Views для уведомлений.
"""
from rest_framework import viewsets, status, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.conf import settings
from .models import Notification, NotificationPreference, ParentChildNotificationSettings, PushSubscription
from .serializers import (
NotificationSerializer,
NotificationPreferenceSerializer,
ParentChildNotificationSettingsSerializer,
PushSubscriptionSerializer
)
from .services import TelegramLinkService
import asyncio
class NotificationViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet для уведомлений (только чтение)."""
queryset = Notification.objects.all()
serializer_class = NotificationSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Только in_app уведомления текущего пользователя."""
queryset = Notification.objects.filter(
recipient=self.request.user,
channel='in_app' # Показываем только in_app уведомления
)
# Оптимизация: для списка используем only() для ограничения полей
# Не используем select_related для recipient, так как он не нужен в сериализаторе
if self.action == 'list':
return queryset.only(
'id', 'notification_type', 'channel', 'priority',
'title', 'message', 'data', 'action_url',
'is_read', 'read_at', 'is_sent', 'created_at'
).order_by('is_read', '-created_at') # сначала непрочитанные, затем по дате
# Для детального просмотра можно использовать select_related, если нужно
return queryset.order_by('is_read', '-created_at')
@action(detail=False, methods=['get'])
def unread(self, request):
"""Получить непрочитанные уведомления."""
queryset = self.get_queryset().filter(is_read=False)
# Оптимизация: используем len() вместо count() для уже загруженного queryset
serializer = self.get_serializer(queryset, many=True)
return Response({
'success': True,
'data': serializer.data,
'count': len(serializer.data) # Используем len() вместо count() для оптимизации
})
@action(detail=True, methods=['post'])
def mark_as_read(self, request, pk=None):
"""Отметить как прочитанное."""
notification = self.get_object()
notification.mark_as_read()
return Response({
'success': True,
'message': 'Уведомление отмечено как прочитанное'
})
@action(detail=False, methods=['post'])
def mark_all_as_read(self, request):
"""Отметить все как прочитанные."""
from django.core.cache import cache
count = self.get_queryset().filter(is_read=False).update(is_read=True)
# Очищаем кеш дашборда для обновления счетчика непрочитанных уведомлений
user_id = request.user.id
cache.delete(f'mentor_dashboard_{user_id}')
cache.delete(f'client_dashboard_{user_id}')
cache.delete(f'parent_dashboard_{user_id}')
return Response({
'success': True,
'message': f'Отмечено {count} уведомлений'
})
class NotificationPreferenceViewSet(viewsets.ModelViewSet):
"""ViewSet для настроек уведомлений."""
queryset = NotificationPreference.objects.all()
serializer_class = NotificationPreferenceSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Только настройки текущего пользователя."""
queryset = NotificationPreference.objects.filter(user=self.request.user).select_related('user')
# Оптимизация: для списка используем only() для ограничения полей
if self.action == 'list':
queryset = queryset.only(
'id', 'user_id', 'enabled', 'email_enabled', 'telegram_enabled',
'in_app_enabled', 'type_preferences', 'quiet_hours_enabled',
'quiet_hours_start', 'quiet_hours_end', 'created_at', 'updated_at'
)
return queryset
@action(detail=False, methods=['get', 'put', 'patch'])
def me(self, request):
"""Получить или обновить свои настройки."""
try:
preferences = request.user.notification_preferences
except:
# Создаем если нет
from .services import create_notification_preferences
create_notification_preferences(request.user)
preferences = request.user.notification_preferences
if request.method == 'GET':
serializer = self.get_serializer(preferences)
return Response({
'success': True,
'data': serializer.data
})
else: # PUT, PATCH
serializer = self.get_serializer(
preferences,
data=request.data,
partial=request.method == 'PATCH'
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({
'success': True,
'message': 'Настройки обновлены',
'data': serializer.data
})
@action(detail=False, methods=['get'], url_path='telegram/bot-info')
def get_telegram_bot_info(self, request):
"""
Получение информации о Telegram боте (username, ссылка).
GET /api/notifications/preferences/telegram/bot-info/
"""
try:
import logging
from telegram import Bot
from telegram.error import TelegramError
logger = logging.getLogger(__name__)
if not getattr(settings, 'TELEGRAM_BOT_TOKEN', None) or not settings.TELEGRAM_BOT_TOKEN.strip():
return Response({
'success': False,
'error': 'TELEGRAM_BOT_TOKEN не настроен'
}, status=status.HTTP_200_OK)
async def get_bot_info():
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
try:
me = await bot.get_me()
return {
'username': me.username,
'first_name': me.first_name,
'id': me.id,
'link': f'https://t.me/{me.username}'
}
finally:
# Игнорируем ошибки при закрытии (rate limiting)
try:
await bot.close()
except (TelegramError, Exception) as close_error:
logger.warning(f'Ошибка при закрытии бота (игнорируется): {close_error}')
bot_info = asyncio.run(get_bot_info())
return Response({
'success': True,
**bot_info
})
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f'Ошибка при получении информации о Telegram боте: {str(e)}', exc_info=True)
return Response({
'success': False,
'error': f'Не удалось получить информацию о боте: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'], url_path='telegram/generate-code')
def generate_telegram_code(self, request):
"""
Генерация кода для связывания Telegram аккаунта.
POST /api/notifications/preferences/telegram/generate-code/
"""
code = TelegramLinkService.generate_link_code(request.user.id)
return Response({
'success': True,
'code': code,
'message': 'Код сгенерирован. Действителен 15 минут.',
'instructions': (
f'1. Откройте Telegram бота\n'
f'2. Отправьте команду: /link {code}\n'
f'3. Ваш аккаунт будет связан'
)
})
@action(detail=False, methods=['post'], url_path='telegram/unlink')
def unlink_telegram(self, request):
"""
Отвязка Telegram аккаунта.
POST /api/notifications/preferences/telegram/unlink/
"""
user = request.user
if not user.telegram_id:
return Response({
'success': False,
'error': 'Telegram аккаунт не привязан'
}, status=status.HTTP_400_BAD_REQUEST)
user.telegram_id = None
user.telegram_username = ''
user.save(update_fields=['telegram_id', 'telegram_username'])
# Выключаем Telegram уведомления
try:
preferences = user.notification_preferences
preferences.telegram_enabled = False
preferences.save(update_fields=['telegram_enabled'])
except:
pass
return Response({
'success': True,
'message': 'Telegram аккаунт успешно отвязан'
})
@action(detail=False, methods=['get'], url_path='telegram/status')
def telegram_status(self, request):
"""
Проверка статуса связывания Telegram.
GET /api/notifications/preferences/telegram/status/
"""
user = request.user
return Response({
'success': True,
'linked': bool(user.telegram_id),
'telegram_id': user.telegram_id,
'telegram_username': user.telegram_username,
'notifications_enabled': (
user.notification_preferences.telegram_enabled
if hasattr(user, 'notification_preferences')
else False
)
})
class ParentChildNotificationSettingsViewSet(viewsets.ModelViewSet):
"""ViewSet для настроек уведомлений родителя для детей."""
serializer_class = ParentChildNotificationSettingsSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Только настройки текущего родителя."""
user = self.request.user
if user.role != 'parent':
return ParentChildNotificationSettings.objects.none()
try:
parent = user.parent_profile
except:
return ParentChildNotificationSettings.objects.none()
queryset = ParentChildNotificationSettings.objects.filter(
parent=parent
).select_related(
'parent', 'parent__user',
'child', 'child__user'
)
# Фильтр по ребенку
child_id = self.request.query_params.get('child_id')
if child_id:
try:
from apps.users.models import Client
child = Client.objects.get(user_id=child_id)
# Проверяем, что ребенок связан с родителем
if child in parent.children.all():
queryset = queryset.filter(child=child)
else:
queryset = ParentChildNotificationSettings.objects.none()
except Client.DoesNotExist:
queryset = ParentChildNotificationSettings.objects.none()
except Exception:
queryset = ParentChildNotificationSettings.objects.none()
return queryset
def perform_create(self, serializer):
"""Создание настроек с автоматическим определением родителя."""
user = self.request.user
if user.role != 'parent':
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Только родители могут создавать настройки уведомлений")
parent = user.parent_profile
serializer.save(parent=parent)
@action(detail=False, methods=['get', 'post', 'put', 'patch'], url_path='for_child')
def for_child(self, request):
"""
Получить или обновить настройки уведомлений для конкретного ребенка.
GET /api/notifications/parent-child-settings/for_child/?child_id=17
POST/PUT/PATCH /api/notifications/parent-child-settings/for_child/?child_id=17
Body: {
"enabled": true,
"type_settings": {
"lesson_created": true,
"homework_assigned": false,
...
}
}
"""
user = request.user
if user.role != 'parent':
return Response(
{'error': 'Только родители могут управлять настройками уведомлений'},
status=status.HTTP_403_FORBIDDEN
)
child_id = request.query_params.get('child_id')
if not child_id:
return Response(
{'error': 'Параметр child_id обязателен'},
status=status.HTTP_400_BAD_REQUEST
)
try:
from apps.users.models import Client, Parent
parent = user.parent_profile
if not parent:
return Response(
{'error': 'Профиль родителя не найден'},
status=status.HTTP_404_NOT_FOUND
)
# Получаем ребенка по user_id и проверяем, что он связан с родителем
try:
child = Client.objects.get(user_id=child_id)
except Client.DoesNotExist:
return Response(
{'error': 'Ребенок не найден'},
status=status.HTTP_404_NOT_FOUND
)
# Проверяем, что ребенок связан с этим родителем
if child not in parent.children.all():
return Response(
{'error': 'Ребенок не связан с этим родителем'},
status=status.HTTP_403_FORBIDDEN
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f'Ошибка в for_child: {str(e)}', exc_info=True)
return Response(
{'error': f'Ошибка при получении данных: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Получаем или создаем настройки
settings, created = ParentChildNotificationSettings.objects.get_or_create(
parent=parent,
child=child,
defaults={'enabled': True, 'type_settings': {}}
)
if request.method == 'GET':
serializer = self.get_serializer(settings)
return Response(serializer.data)
# Обновление настроек
serializer = self.get_serializer(settings, data=request.data, partial=request.method == 'PATCH')
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class PushSubscriptionView(generics.GenericAPIView):
"""
API endpoint для сохранения Push Notification subscription.
POST /api/notifications/push-subscription/
"""
serializer_class = PushSubscriptionSerializer
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
"""Сохранение или обновление Push subscription."""
user = request.user
subscription_data = request.data.get('subscription_data')
user_agent = request.META.get('HTTP_USER_AGENT', '')
if not subscription_data:
return Response({
'success': False,
'error': 'subscription_data обязателен'
}, status=status.HTTP_400_BAD_REQUEST)
# Проверяем, есть ли уже такая subscription
# Используем endpoint из subscription_data как уникальный идентификатор
endpoint = subscription_data.get('endpoint')
if endpoint:
existing = PushSubscription.objects.filter(
user=user,
subscription_data__endpoint=endpoint
).first()
if existing:
# Обновляем существующую
existing.subscription_data = subscription_data
existing.user_agent = user_agent
existing.is_active = True
existing.save()
return Response({
'success': True,
'message': 'Push subscription обновлена',
'subscription_id': existing.id
}, status=status.HTTP_200_OK)
# Создаем новую subscription
subscription = PushSubscription.objects.create(
user=user,
subscription_data=subscription_data,
user_agent=user_agent
)
return Response({
'success': True,
'message': 'Push subscription сохранена',
'subscription_id': subscription.id
}, status=status.HTTP_201_CREATED)
def delete(self, request, *args, **kwargs):
"""Удаление Push subscription."""
user = request.user
endpoint = request.data.get('endpoint')
if not endpoint:
return Response({
'success': False,
'error': 'endpoint обязателен'
}, status=status.HTTP_400_BAD_REQUEST)
# Деактивируем subscription
count = PushSubscription.objects.filter(
user=user,
subscription_data__endpoint=endpoint
).update(is_active=False)
return Response({
'success': True,
'message': f'Деактивировано {count} subscription(s)'
}, status=status.HTTP_200_OK)