diff --git a/apps/account/notification_service.py b/apps/account/notification_service.py index f46991a..7958280 100644 --- a/apps/account/notification_service.py +++ b/apps/account/notification_service.py @@ -1,12 +1,17 @@ import asyncio import logging +import string from apps.account.models import Notification, NotificationTemplate logger = logging.getLogger(__name__) -class SafeDict(dict): - def __missing__(self, key): - return f"{{{key}}}" +class SafeFormatter(string.Formatter): + def get_value(self, key, args, kwargs): + if isinstance(key, str): + if key not in kwargs: + return f"{{{key}}}" + return kwargs[key] + return super().get_value(key, args, kwargs) def get_template_context(user, notification_type, data): """ @@ -198,13 +203,13 @@ def create_and_send_notification(user, title_en, body_en, title_fa=None, body_fa # 2. Render templates safely with dynamic context context = get_template_context(user, notification_type, data) - safe_ctx = SafeDict(context) + formatter = SafeFormatter() try: - title_ru = title_ru.format(**safe_ctx) if title_ru else "" - title_en = title_en.format(**safe_ctx) if title_en else "" - body_ru = body_ru.format(**safe_ctx) if body_ru else "" - body_en = body_en.format(**safe_ctx) if body_en else "" + title_ru = formatter.format(title_ru, **context) if title_ru else "" + title_en = formatter.format(title_en, **context) if title_en else "" + body_ru = formatter.format(body_ru, **context) if body_ru else "" + body_en = formatter.format(body_en, **context) if body_en else "" except Exception as e: logger.error(f"Error formatting templates: {e}") diff --git a/apps/account/urls.py b/apps/account/urls.py index 8596931..a4fe4a1 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -46,6 +46,7 @@ urlpatterns = [ path('notif/', views.NotificationListView.as_view(), name='user-notif'), path('notif/read', views.NotificationReadAllView.as_view(), name='user-notif-read-all'), path('notif/send/', views.SendNotificationView.as_view(), name='user-send-notif'), + path('notifications/test/', views.TestNotificationAPIView.as_view(), name='test-notifications'), # # URL to update user details, supports PUT to update user fields like phone or email given a token. path('profile/update/', views.UserUpdateView.as_view(), name='user-update'), diff --git a/apps/account/views/notification.py b/apps/account/views/notification.py index be1a41a..4854e39 100644 --- a/apps/account/views/notification.py +++ b/apps/account/views/notification.py @@ -242,3 +242,141 @@ class AdminNotificationViewSet(ModelViewSet): 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)