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.
261 lines
11 KiB
261 lines
11 KiB
import asyncio
|
|
import logging
|
|
import string
|
|
from apps.account.models import Notification, NotificationTemplate
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
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):
|
|
"""
|
|
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)
|
|
formatter = SafeFormatter()
|
|
|
|
try:
|
|
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}")
|
|
|
|
# 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
|