From e77acb113e641fdcc37e994d88c83f02e5595ccd Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sat, 11 Jul 2026 13:09:00 +0330 Subject: [PATCH] notifications navigation and type fields added --- ...ction_notification_navigate_to_and_more.py | 28 ++++++ apps/account/models/notification.py | 3 + apps/account/notification_service.py | 92 ++++++++++++++++++- .../tests/notifications/test_notifications.py | 12 +++ 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 apps/account/migrations/0006_notification_action_notification_navigate_to_and_more.py diff --git a/apps/account/migrations/0006_notification_action_notification_navigate_to_and_more.py b/apps/account/migrations/0006_notification_action_notification_navigate_to_and_more.py new file mode 100644 index 0000000..ad0ff57 --- /dev/null +++ b/apps/account/migrations/0006_notification_action_notification_navigate_to_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.12 on 2026-07-11 12:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_user_password_enc'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='action', + field=models.CharField(default='navigate', max_length=50, verbose_name='action'), + ), + migrations.AddField( + model_name='notification', + name='navigate_to', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='navigate to'), + ), + migrations.AddField( + model_name='notification', + name='notification_type', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='notification type'), + ), + ] diff --git a/apps/account/models/notification.py b/apps/account/models/notification.py index d9394a7..cae7a4e 100644 --- a/apps/account/models/notification.py +++ b/apps/account/models/notification.py @@ -17,6 +17,9 @@ class Notification(models.Model): default=ServiceChoices.IMAM_JAVAD, verbose_name=_('service') ) + notification_type = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('notification type')) + action = models.CharField(max_length=50, default='navigate', verbose_name=_('action')) + navigate_to = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('navigate to')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'), null=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'), null=True) diff --git a/apps/account/notification_service.py b/apps/account/notification_service.py index 6a3c2e9..a9d5a25 100644 --- a/apps/account/notification_service.py +++ b/apps/account/notification_service.py @@ -5,7 +5,74 @@ from apps.account.tasks import send_notification logger = logging.getLogger(__name__) -def create_and_send_notification(user, title_en, body_en, title_fa, body_fa, service='imam-javad', data=None): +def resolve_notification_route(notification_type, data): + """ + Resolves standard GoRouter paths based on notification type and payload data. + """ + if not data or not isinstance(data, dict): + return None + + # Import models dynamically inside function to avoid circular imports + from apps.course.models.course import Course + from apps.course.models.live_session import CourseLiveSession + + try: + # Group 1: Course related paths (/course_info?slug=...) + if notification_type in ['course_registered', 'course_completed', 'new_course_weekly', + 'live_class_rescheduled', 'live_class_cancelled', + 'live_recording_available', 'missed_live_sessions', + 'course_access_granted']: + course_id = data.get('course_id') + if not course_id and 'session_id' in data: + session = CourseLiveSession.objects.filter(id=data['session_id']).first() + if session: + course_id = session.course_id + + if course_id: + course = Course.objects.filter(id=course_id).first() + if course: + slug = course.slug[0].get('title') if isinstance(course.slug, list) and course.slug else course.slug + return f"/course_info?slug={slug}" + + # Group 2: Lesson path (/lesson?slug=...) + elif notification_type == 'lesson_completed': + course_id = data.get('course_id') + if course_id: + course = Course.objects.filter(id=course_id).first() + if course: + slug = course.slug[0].get('title') if isinstance(course.slug, list) and course.slug else course.slug + return f"/lesson?slug={slug}" + + # Group 3: Quiz path (/course_info/quiz?slug=...) + elif notification_type == 'low_quiz_result': + from apps.quiz.models import Quiz + quiz = Quiz.objects.filter(id=data.get('quiz_id')).first() + if quiz and quiz.course: + course = quiz.course + slug = course.slug[0].get('title') if isinstance(course.slug, list) and course.slug else course.slug + return f"/course_info/quiz?slug={slug}" + + # Group 4: Chat Room path (/chat/chat_details?room_id=...) + elif notification_type in ['teacher_reply', 'support_reply']: + room_id = data.get('room_id') + if room_id: + return f"/chat/chat_details?room_id={room_id}" + + # Group 5: Transactions path + elif notification_type in ['payment_successful', 'payment_failed', 'refund_completed']: + return "/profile/transactions" + + # Group 6: Home path + elif notification_type == 'student_inactivity': + return "/home" + + except Exception as e: + logger.error(f"Error resolving route for type={notification_type}, data={data}: {e}") + + return None + +def create_and_send_notification(user, title_en, body_en, title_fa, body_fa, service='imam-javad', + data=None, notification_type=None, action='navigate', navigate_to=None): """ Creates a Notification record in the database and sends a push notification to FCM. Resolves localized title and body based on user.language. @@ -16,16 +83,33 @@ def create_and_send_notification(user, title_en, body_en, title_fa, body_fa, ser title = title_fa if lang_code == 'fa' else title_en message = body_fa if lang_code == 'fa' else body_en + # Auto-resolve notification type from data payload if not provided + if not notification_type and isinstance(data, dict): + notification_type = data.get('type') + + # Auto-resolve GoRouter navigation route if not provided + if not navigate_to and notification_type: + navigate_to = resolve_notification_route(notification_type, data) + # Save to database notif = Notification.objects.create( user=user, title=title, message=message, - service=service + service=service, + notification_type=notification_type, + action=action, + navigate_to=navigate_to ) fcm_token = getattr(user, 'fcm', None) if fcm_token: + # Prepare the payload including the new routing properties + fcm_data = dict(data or {}) + fcm_data.setdefault('type', notification_type or '') + fcm_data.setdefault('action', action or '') + fcm_data.setdefault('navigate_to', navigate_to or '') + try: try: loop = asyncio.get_event_loop() @@ -34,9 +118,9 @@ def create_and_send_notification(user, title_en, body_en, title_fa, body_fa, ser asyncio.set_event_loop(loop) if loop.is_running(): - loop.create_task(send_notification([fcm_token], title, message, data)) + loop.create_task(send_notification([fcm_token], title, message, fcm_data)) else: - loop.run_until_complete(send_notification([fcm_token], title, message, data)) + loop.run_until_complete(send_notification([fcm_token], title, message, fcm_data)) except Exception as e: logger.error(f"Failed to send push notification via FCM: {e}") diff --git a/apps/account/tests/notifications/test_notifications.py b/apps/account/tests/notifications/test_notifications.py index 64d94e9..1c479b3 100644 --- a/apps/account/tests/notifications/test_notifications.py +++ b/apps/account/tests/notifications/test_notifications.py @@ -87,6 +87,9 @@ class NotificationFlowTests(TestCase): self.assertIsNotNone(notif_fa) self.assertEqual(notif_fa.title, "دسترسی به دوره فعال شد") self.assertIn("دوره ۱", notif_fa.message) + self.assertEqual(notif_fa.notification_type, "course_access_granted") + self.assertEqual(notif_fa.action, "navigate") + self.assertEqual(notif_fa.navigate_to, "/course_info?slug=course-access") # Create participant for English student Participant.objects.create(student=self.student_en, course=course) @@ -94,9 +97,18 @@ class NotificationFlowTests(TestCase): self.assertIsNotNone(notif_en) self.assertEqual(notif_en.title, "Course Access Granted") self.assertIn("Course 1", notif_en.message) + self.assertEqual(notif_en.notification_type, "course_access_granted") + self.assertEqual(notif_en.action, "navigate") + self.assertEqual(notif_en.navigate_to, "/course_info?slug=course-access") # Verify send_notification was called self.assertTrue(mock_send.called) + # Check that target routing keys were passed to send_notification call + called_args, called_kwargs = mock_send.call_args + sent_data = called_args[3] if len(called_args) > 3 else called_kwargs.get('data') + self.assertEqual(sent_data.get('type'), "course_access_granted") + self.assertEqual(sent_data.get('action'), "navigate") + self.assertEqual(sent_data.get('navigate_to'), "/course_info?slug=course-access") @patch('apps.account.notification_service.send_notification') def test_live_class_reminder_notification(self, mock_send):