Browse Source

notifications navigation and type fields added

master
Mohsen Taba 1 week ago
parent
commit
e77acb113e
  1. 28
      apps/account/migrations/0006_notification_action_notification_navigate_to_and_more.py
  2. 3
      apps/account/models/notification.py
  3. 92
      apps/account/notification_service.py
  4. 12
      apps/account/tests/notifications/test_notifications.py

28
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'),
),
]

3
apps/account/models/notification.py

@ -17,6 +17,9 @@ class Notification(models.Model):
default=ServiceChoices.IMAM_JAVAD, default=ServiceChoices.IMAM_JAVAD,
verbose_name=_('service') 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) 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) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'), null=True)

92
apps/account/notification_service.py

@ -5,7 +5,74 @@ from apps.account.tasks import send_notification
logger = logging.getLogger(__name__) 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. Creates a Notification record in the database and sends a push notification to FCM.
Resolves localized title and body based on user.language. 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 title = title_fa if lang_code == 'fa' else title_en
message = body_fa if lang_code == 'fa' else body_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 # Save to database
notif = Notification.objects.create( notif = Notification.objects.create(
user=user, user=user,
title=title, title=title,
message=message, message=message,
service=service
service=service,
notification_type=notification_type,
action=action,
navigate_to=navigate_to
) )
fcm_token = getattr(user, 'fcm', None) fcm_token = getattr(user, 'fcm', None)
if fcm_token: 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:
try: try:
loop = asyncio.get_event_loop() 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) asyncio.set_event_loop(loop)
if loop.is_running(): 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: 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: except Exception as e:
logger.error(f"Failed to send push notification via FCM: {e}") logger.error(f"Failed to send push notification via FCM: {e}")

12
apps/account/tests/notifications/test_notifications.py

@ -87,6 +87,9 @@ class NotificationFlowTests(TestCase):
self.assertIsNotNone(notif_fa) self.assertIsNotNone(notif_fa)
self.assertEqual(notif_fa.title, "دسترسی به دوره فعال شد") self.assertEqual(notif_fa.title, "دسترسی به دوره فعال شد")
self.assertIn("دوره ۱", notif_fa.message) 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 # Create participant for English student
Participant.objects.create(student=self.student_en, course=course) Participant.objects.create(student=self.student_en, course=course)
@ -94,9 +97,18 @@ class NotificationFlowTests(TestCase):
self.assertIsNotNone(notif_en) self.assertIsNotNone(notif_en)
self.assertEqual(notif_en.title, "Course Access Granted") self.assertEqual(notif_en.title, "Course Access Granted")
self.assertIn("Course 1", notif_en.message) 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 # Verify send_notification was called
self.assertTrue(mock_send.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') @patch('apps.account.notification_service.send_notification')
def test_live_class_reminder_notification(self, mock_send): def test_live_class_reminder_notification(self, mock_send):

Loading…
Cancel
Save