""" 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 settings.TELEGRAM_BOT_TOKEN: return Response({ 'success': False, 'error': 'TELEGRAM_BOT_TOKEN не настроен' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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)