You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
19 KiB
422 lines
19 KiB
from rest_framework import generics, status
|
|
from rest_framework.response import Response
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from drf_yasg import openapi
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from apps.account.serializers import NotificationSerializer, NotificationSendSerializer, AdminNotificationSerializer, NotificationTemplateSerializer, AdminScheduledNotificationSerializer
|
|
from apps.account.models import Notification, User, NotificationTemplate, ScheduledNotification
|
|
from apps.account.tasks import send_notification
|
|
from utils.pagination import StandardResultsSetPagination
|
|
from django.db.models import Q
|
|
from rest_framework.viewsets import ModelViewSet
|
|
from apps.account.permissions import IsSuperAdmin
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
class NotificationListView(generics.ListAPIView):
|
|
queryset = Notification.objects.all()
|
|
serializer_class = NotificationSerializer
|
|
permission_classes = [IsAuthenticated,]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Retrieve a list of notifications for the authenticated user or merchant account.",
|
|
tags=['Notifications'],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'service',
|
|
openapi.IN_QUERY,
|
|
description="Filter notifications by service (imam-javad or doboodi)",
|
|
type=openapi.TYPE_STRING,
|
|
enum=['imam-javad', 'doboodi'],
|
|
required=False
|
|
)
|
|
]
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
"""
|
|
This API allows you to retrieve a list of notifications based on the authenticated user's type.
|
|
If the user is a regular user, their notifications will be fetched from the `Notification` model.
|
|
If the user is a merchant, their notifications will be fetched from the `MerchantAccountNotification` model.
|
|
- **Method**: GET
|
|
- **URL**: /api/notifications/
|
|
- **Query Parameters**:
|
|
- `service`: Optional. Filter notifications by service ('imam-javad' or 'doboodi')
|
|
- **Response**: Includes details of notifications such as title, message, is read status, service, creation date, and update date.
|
|
- **Headers**: `Authorization: Bearer <token>` for authentication.
|
|
"""
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
queryset = Notification.objects.filter(user=user)
|
|
|
|
# Filter by service if provided in query params
|
|
service = self.request.query_params.get('service', None)
|
|
if service:
|
|
queryset = queryset.filter(service=service)
|
|
|
|
return queryset.order_by('-created_at')
|
|
|
|
|
|
class NotificationReadAllView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated,]
|
|
authentication_classes = [TokenAuthentication]
|
|
queryset = Notification.objects.all()
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Mark all notifications as read for the authenticated user.",
|
|
tags=['Notifications'],
|
|
responses={
|
|
200: "All notifications marked as read",
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
user = request.user
|
|
|
|
# Mark all of the user's notifications as read
|
|
Notification.objects.filter(user=user).update(is_read=True)
|
|
|
|
return Response({'status': 'all notifications marked as read'}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class SendNotificationView(generics.GenericAPIView):
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Send a notification to a user by user_id.",
|
|
tags=['Notifications'],
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=['user_id', 'title', 'body'],
|
|
properties={
|
|
'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the user to send notification to'),
|
|
'title': openapi.Schema(type=openapi.TYPE_STRING, description='Notification title'),
|
|
'body': openapi.Schema(type=openapi.TYPE_STRING, description='Notification body'),
|
|
'data': openapi.Schema(type=openapi.TYPE_OBJECT, description='Additional data payload', default={'slam': 'qatreh'}),
|
|
},
|
|
),
|
|
responses={
|
|
200: openapi.Response('Notification sent successfully.'),
|
|
400: openapi.Response('FCM token not available for this user.'),
|
|
404: openapi.Response('User not found.'),
|
|
500: openapi.Response('Internal server error.'),
|
|
}
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
user_id = request.data.get('user_id', 1)
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
notification_title = request.data.get('title', 'test qatreh')
|
|
notification_body = request.data.get('body', 'test qatreh body')
|
|
data_payload = request.data.get('data', {'slam':'qatreh'})
|
|
|
|
fcm_token = user.fcm # Ensure that 'fcm' is a field in your User model
|
|
|
|
if not fcm_token:
|
|
return Response({
|
|
'error': 'FCM token not available for this user.'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
send_notification([fcm_token], notification_title, notification_body, data_payload)
|
|
return Response({
|
|
'message': 'Notification sent successfully.'
|
|
}, status=status.HTTP_200_OK)
|
|
except Exception as e:
|
|
return Response({
|
|
'error': str(e)
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class AdminNotificationViewSet(ModelViewSet):
|
|
permission_classes = [IsSuperAdmin]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminNotificationSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = Notification.objects.all().select_related('user')
|
|
|
|
# Filter by service if provided
|
|
service = self.request.query_params.get('service', None)
|
|
if service:
|
|
queryset = queryset.filter(service=service)
|
|
|
|
# Search by user name or email or notification title/message
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search) |
|
|
Q(message__icontains=search) |
|
|
Q(user__fullname__icontains=search) |
|
|
Q(user__email__icontains=search)
|
|
)
|
|
|
|
# Date filtering
|
|
date_after = self.request.query_params.get('date_after', None)
|
|
if date_after:
|
|
queryset = queryset.filter(created_at__date__gte=date_after)
|
|
|
|
date_before = self.request.query_params.get('date_before', None)
|
|
if date_before:
|
|
queryset = queryset.filter(created_at__date__lte=date_before)
|
|
|
|
# User filtering
|
|
user_param = self.request.query_params.get('user', None)
|
|
if user_param:
|
|
queryset = queryset.filter(user_id=user_param)
|
|
|
|
# Read status filtering (is_read=true/false)
|
|
is_read_param = self.request.query_params.get('is_read', None)
|
|
if is_read_param is not None:
|
|
if is_read_param.lower() == 'true':
|
|
queryset = queryset.filter(is_read=True)
|
|
elif is_read_param.lower() == 'false':
|
|
queryset = queryset.filter(is_read=False)
|
|
|
|
return queryset.order_by('-created_at')
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
title = request.data.get('title')
|
|
message = request.data.get('message')
|
|
service = request.data.get('service', 'imam-javad')
|
|
send_to_all = request.data.get('send_to_all', False)
|
|
user_id = request.data.get('user_id', None)
|
|
|
|
if not title or not message:
|
|
return Response({'error': 'Title and message are required.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if send_to_all:
|
|
# Get all active users
|
|
users = User.objects.filter(is_active=True)
|
|
notifications_to_create = []
|
|
for u in users:
|
|
notifications_to_create.append(Notification(
|
|
title=title,
|
|
message=message,
|
|
service=service,
|
|
user=u
|
|
))
|
|
Notification.objects.bulk_create(notifications_to_create)
|
|
|
|
# Send push notifications
|
|
fcm_tokens = list(users.exclude(fcm__isnull=True).exclude(fcm='').values_list('fcm', flat=True))
|
|
if fcm_tokens:
|
|
try:
|
|
send_notification(fcm_tokens, title, message, {'service': service})
|
|
except Exception as e:
|
|
logger.error(f"Failed to send push notifications: {e}")
|
|
|
|
return Response({'message': f'Notification created for {len(notifications_to_create)} users.'}, status=status.HTTP_201_CREATED)
|
|
else:
|
|
if not user_id:
|
|
return Response({'error': 'user_id is required when send_to_all is false.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
notif = Notification.objects.create(
|
|
title=title,
|
|
message=message,
|
|
service=service,
|
|
user=user
|
|
)
|
|
|
|
# Send push notification
|
|
if user.fcm:
|
|
try:
|
|
send_notification([user.fcm], title, message, {'service': service})
|
|
except Exception as e:
|
|
logger.error(f"Failed to send push notification: {e}")
|
|
|
|
serializer = self.get_serializer(notif)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class TestNotificationAPIView(generics.GenericAPIView):
|
|
permission_classes = []
|
|
authentication_classes = []
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Send all test notifications to a user by their email.",
|
|
tags=['Notifications'],
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=['email'],
|
|
properties={
|
|
'email': openapi.Schema(type=openapi.TYPE_STRING, description='Email of the user to send test notifications to'),
|
|
},
|
|
),
|
|
responses={
|
|
200: openapi.Response('All test notifications processed successfully.'),
|
|
400: openapi.Response('Email parameter missing.'),
|
|
404: openapi.Response('User not found.'),
|
|
}
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
email = request.data.get('email')
|
|
if not email:
|
|
return Response({'error': 'Email parameter is required.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
from apps.account.models import User
|
|
user = User.objects.filter(email=email).first()
|
|
if not user:
|
|
return Response({'error': f"User with email '{email}' not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
from apps.account.notification_service import create_and_send_notification
|
|
|
|
notifications_to_send = [
|
|
{
|
|
"name": "1. Course Registered",
|
|
"title_en": "Course Registration Successful",
|
|
"body_en": "You have successfully registered for the course 'Quranic Studies'.",
|
|
"title_ru": "Успешная регистрация на курс",
|
|
"body_ru": "Вы успешно зарегистрировались на курс «مطالعات قرآنی».",
|
|
"data": {"type": "course_registered", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "2. Course Access Granted",
|
|
"title_en": "Course Access Granted",
|
|
"body_en": "Your access to the course 'Quranic Studies' has been activated.",
|
|
"title_ru": "Доступ к курсу активирован",
|
|
"body_ru": "Ваш доступ к курсу «مطالعات قرآنی» успешно активирован.",
|
|
"data": {"type": "course_access_granted", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "3. Lesson Completed",
|
|
"title_en": "Lesson Completed",
|
|
"body_en": "Congratulations! You completed the lesson 'Introduction to Surah Al-Fatiha'.",
|
|
"title_ru": "Урок завершен",
|
|
"body_ru": "Поздравляем! Вы завершили изучение урока «آشنایی با سوره فاتحه».",
|
|
"data": {"type": "lesson_completed", "course_id": "1", "lesson_id": "1"}
|
|
},
|
|
{
|
|
"name": "4. Course Completed",
|
|
"title_en": "Course Completed",
|
|
"body_en": "Well done! You have completed the course 'Quranic Studies'.",
|
|
"title_ru": "Курс завершен",
|
|
"body_ru": "Отличная работа! Вы успешно завершили курс «مطالعات قرآنی».",
|
|
"data": {"type": "course_completed", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "5. Low Quiz Score Suggestion",
|
|
"title_en": "Quiz Retake Suggestion",
|
|
"body_en": "You scored below the passing threshold on quiz 'Final Exam'. We suggest taking it again.",
|
|
"title_ru": "Предложение пересдать тест",
|
|
"body_ru": "Вы набрали балл ниже проходного в тесте «امتحان نهایی». Рекомендуем пройти его повторно.",
|
|
"data": {"type": "low_quiz_result", "quiz_id": "1"}
|
|
},
|
|
{
|
|
"name": "6. Payment Successful",
|
|
"title_en": "Payment Successful",
|
|
"body_en": "Your payment for course 'Quranic Studies' was successful.",
|
|
"title_ru": "Оплата успешна",
|
|
"body_ru": "Ваша оплата за курс «مطالعات قرآنی» успешно проведена.",
|
|
"data": {"type": "payment_successful", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "7. Payment Failed",
|
|
"title_en": "Payment Failed",
|
|
"body_en": "We encountered an issue processing your payment for 'Quranic Studies'.",
|
|
"title_ru": "Ошибка оплаты",
|
|
"body_ru": "Произошла ошибка при обработке вашей оплаты за курс «مطالعات قرآنی».",
|
|
"data": {"type": "payment_failed", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "8. Refund Completed",
|
|
"title_en": "Refund Completed",
|
|
"body_en": "A refund has been successfully completed for the course 'Quranic Studies'.",
|
|
"title_ru": "Возврат средств выполнен",
|
|
"body_ru": "Возврат средств за курс «مطالعات قرآنی» успешно завершен.",
|
|
"data": {"type": "refund_completed", "course_id": "1"}
|
|
},
|
|
{
|
|
"name": "9. Student Inactivity",
|
|
"title_en": "We Miss You!",
|
|
"body_en": "You haven't taken any lessons in the last week. Come back and continue your learning journey!",
|
|
"title_ru": "Мы скучаем по вам!",
|
|
"body_ru": "Вы не изучали уроки за последнюю неделю. Ждем вас для продолжения обучения!",
|
|
"data": {"type": "student_inactivity"}
|
|
}
|
|
]
|
|
|
|
results = []
|
|
for item in notifications_to_send:
|
|
try:
|
|
notif = create_and_send_notification(
|
|
user=user,
|
|
title_en=item['title_en'],
|
|
body_en=item['body_en'],
|
|
title_ru=item['title_ru'],
|
|
body_ru=item['body_ru'],
|
|
service='imam-javad',
|
|
data=item['data']
|
|
)
|
|
results.append({
|
|
'name': item['name'],
|
|
'status': 'sent' if notif else 'disabled',
|
|
'notification_id': notif.id if notif else None
|
|
})
|
|
except Exception as e:
|
|
results.append({
|
|
'name': item['name'],
|
|
'status': 'failed',
|
|
'error': str(e)
|
|
})
|
|
|
|
return Response({
|
|
'message': 'All test notifications processed successfully.',
|
|
'results': results,
|
|
'fcm_token_registered': bool(user.fcm)
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class AdminNotificationTemplateViewSet(ModelViewSet):
|
|
permission_classes = [IsSuperAdmin]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = NotificationTemplateSerializer
|
|
queryset = NotificationTemplate.objects.all().order_by('id')
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
# Disable create and destroy actions as templates are system-defined
|
|
def create(self, request, *args, **kwargs):
|
|
return Response({'error': 'Method not allowed.'}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
return Response({'error': 'Method not allowed.'}, status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
|
|
|
|
class AdminScheduledNotificationViewSet(ModelViewSet):
|
|
permission_classes = [IsSuperAdmin]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminScheduledNotificationSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = ScheduledNotification.objects.all().select_related('template', 'user')
|
|
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search) |
|
|
Q(title_ru__icontains=search) |
|
|
Q(title_en__icontains=search) |
|
|
Q(template__name__icontains=search)
|
|
)
|
|
|
|
is_active_param = self.request.query_params.get('is_active', None)
|
|
if is_active_param is not None:
|
|
queryset = queryset.filter(is_active=(is_active_param.lower() == 'true'))
|
|
|
|
return queryset.order_by('-id')
|