import asyncio import logging from apps.account.models import Notification, NotificationTemplate logger = logging.getLogger(__name__) class SafeDict(dict): def __missing__(self, key): return f"{{{key}}}" def get_template_context(user, notification_type, data): """ Builds a dynamic context dictionary containing user info and target entity details (course, lesson, quiz, session) to format templates. """ context = { 'student_name': getattr(user, 'fullname', user.username) or user.username, 'fullname': getattr(user, 'fullname', user.username) or user.username, 'username': user.username, } if not data or not isinstance(data, dict): return context # Import models dynamically inside function to avoid circular imports from apps.course.models.course import Course from apps.course.models.lesson import CourseLesson from apps.course.models.live_session import CourseLiveSession from apps.quiz.models import Quiz try: # 1. Course details 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: title_val = course.title # course.title might be a JSON list or dict or string if isinstance(title_val, list) and title_val: course_title = title_val[0].get('title', '') elif isinstance(title_val, dict): course_title = title_val.get('title', '') else: course_title = str(title_val) context['course_title'] = course_title context['course_name'] = course_title slug_val = course.slug if isinstance(slug_val, list) and slug_val: course_slug = slug_val[0].get('title', '') elif isinstance(slug_val, dict): course_slug = slug_val.get('title', '') else: course_slug = str(slug_val) context['course_slug'] = course_slug # 2. Lesson details lesson_id = data.get('lesson_id') if lesson_id: lesson = CourseLesson.objects.filter(id=lesson_id).first() if lesson: title_val = lesson.title if isinstance(title_val, list) and title_val: lesson_title = title_val[0].get('title', '') elif isinstance(title_val, dict): lesson_title = title_val.get('title', '') else: lesson_title = str(title_val) context['lesson_title'] = lesson_title # 3. Quiz details quiz_id = data.get('quiz_id') if quiz_id: quiz = Quiz.objects.filter(id=quiz_id).first() if quiz: title_val = quiz.title if isinstance(title_val, list) and title_val: quiz_title = title_val[0].get('title', '') elif isinstance(title_val, dict): quiz_title = title_val.get('title', '') else: quiz_title = str(title_val) context['quiz_title'] = quiz_title # 4. Live Session details session_id = data.get('session_id') if session_id: session = CourseLiveSession.objects.filter(id=session_id).first() if session: context['session_subject'] = session.subject if session.start_at: context['session_start_time'] = session.start_at.strftime('%H:%M') except Exception as e: logger.error(f"Error building notification template context: {e}") return context 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', 'teacher_reply_private', 'teacher_reply_course', '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=None, body_fa=None, service='imam-javad', data=None, notification_type=None, action='navigate', navigate_to=None, title_ru=None, body_ru=None): """ Creates a Notification record in the database and sends a push notification to FCM. Loads and renders the NotificationTemplate from the database if present. If the template is inactive, the notification is discarded. """ # Map Persian arguments to Russian to preserve backward compatibility with existing signals/tasks title_ru = title_ru or title_fa body_ru = body_ru or body_fa # Auto-resolve notification type from data payload if not provided if not notification_type and isinstance(data, dict): notification_type = data.get('type') # 1. Check for template settings in the database if notification_type: template = NotificationTemplate.objects.filter(notification_type=notification_type).first() if template: if not template.is_active: logger.info(f"Notification type '{notification_type}' is disabled via database template settings.") return None # Override templates with database values title_ru = template.title_ru title_en = template.title_en body_ru = template.body_ru body_en = template.body_en # 2. Render templates safely with dynamic context context = get_template_context(user, notification_type, data) safe_ctx = SafeDict(context) 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 "" except Exception as e: logger.error(f"Error formatting templates: {e}") # Determine user localized text lang = getattr(user, 'language', None) lang_code = lang.code if lang and hasattr(lang, 'code') else 'ru' title = title_ru if lang_code == 'ru' else title_en message = body_ru if lang_code == 'ru' else body_en # 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 from apps.account.tasks import send_notification notif = Notification.objects.create( user=user, title=title, message=message, 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() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) if loop.is_running(): loop.create_task(send_notification([fcm_token], title, message, fcm_data)) else: 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}") return notif