""" Notification service for creating and managing user notifications. This service handles the creation, delivery, and management of notifications for various events including submission approvals/rejections. """ from django.utils import timezone from django.contrib.contenttypes.models import ContentType from django.template.loader import render_to_string from django.core.mail import send_mail from django.conf import settings from django.db import models from typing import Optional, Dict, Any, List import logging from apps.accounts.models.notifications import UserNotification, NotificationPreference from apps.accounts.models import User logger = logging.getLogger(__name__) class NotificationService: """Service for creating and managing user notifications.""" @staticmethod def create_notification( user: User, notification_type: str, title: str, message: str, related_object: Optional[Any] = None, priority: str = UserNotification.Priority.NORMAL, extra_data: Optional[Dict[str, Any]] = None, expires_at: Optional[timezone.datetime] = None, ) -> UserNotification: """ Create a new notification for a user. Args: user: The user to notify notification_type: Type of notification (from UserNotification.NotificationType) title: Notification title message: Notification message related_object: Optional related object (submission, review, etc.) priority: Notification priority extra_data: Additional data to store with notification expires_at: When the notification expires Returns: UserNotification: The created notification """ # Get content type and object ID if related object provided content_type = None object_id = None if related_object: content_type = ContentType.objects.get_for_model(related_object) object_id = related_object.pk # Create the notification notification = UserNotification.objects.create( user=user, notification_type=notification_type, title=title, message=message, content_type=content_type, object_id=object_id, priority=priority, extra_data=extra_data or {}, expires_at=expires_at, ) # Send notification through appropriate channels NotificationService._send_notification(notification) return notification @staticmethod def create_submission_approved_notification( user: User, submission_object: Any, submission_type: str, additional_message: str = "", ) -> UserNotification: """ Create a notification for submission approval. Args: user: User who submitted the content submission_object: The approved submission object submission_type: Type of submission (e.g., "park photo", "ride review") additional_message: Additional message from moderator Returns: UserNotification: The created notification """ title = f"Your {submission_type} has been approved!" message = f"Great news! Your {submission_type} submission has been approved and is now live on ThrillWiki." if additional_message: message += f"\n\nModerator note: {additional_message}" extra_data = { "submission_type": submission_type, "moderator_message": additional_message, "approved_at": timezone.now().isoformat(), } return NotificationService.create_notification( user=user, notification_type=UserNotification.NotificationType.SUBMISSION_APPROVED, title=title, message=message, related_object=submission_object, priority=UserNotification.Priority.NORMAL, extra_data=extra_data, ) @staticmethod def create_submission_rejected_notification( user: User, submission_object: Any, submission_type: str, rejection_reason: str, additional_message: str = "", ) -> UserNotification: """ Create a notification for submission rejection. Args: user: User who submitted the content submission_object: The rejected submission object submission_type: Type of submission (e.g., "park photo", "ride review") rejection_reason: Reason for rejection additional_message: Additional message from moderator Returns: UserNotification: The created notification """ title = f"Your {submission_type} needs attention" message = f"Your {submission_type} submission has been reviewed and needs some changes before it can be approved." message += f"\n\nReason: {rejection_reason}" if additional_message: message += f"\n\nModerator note: {additional_message}" message += "\n\nYou can edit and resubmit your content from your profile page." extra_data = { "submission_type": submission_type, "rejection_reason": rejection_reason, "moderator_message": additional_message, "rejected_at": timezone.now().isoformat(), } return NotificationService.create_notification( user=user, notification_type=UserNotification.NotificationType.SUBMISSION_REJECTED, title=title, message=message, related_object=submission_object, priority=UserNotification.Priority.HIGH, extra_data=extra_data, ) @staticmethod def create_submission_pending_notification( user: User, submission_object: Any, submission_type: str ) -> UserNotification: """ Create a notification for submission pending review. Args: user: User who submitted the content submission_object: The pending submission object submission_type: Type of submission (e.g., "park photo", "ride review") Returns: UserNotification: The created notification """ title = f"Your {submission_type} is under review" message = f"Thanks for your {submission_type} submission! It's now under review by our moderation team." message += "\n\nWe'll notify you once it's been reviewed. This usually takes 1-2 business days." extra_data = { "submission_type": submission_type, "submitted_at": timezone.now().isoformat(), } return NotificationService.create_notification( user=user, notification_type=UserNotification.NotificationType.SUBMISSION_PENDING, title=title, message=message, related_object=submission_object, priority=UserNotification.Priority.LOW, extra_data=extra_data, ) @staticmethod def _send_notification(notification: UserNotification) -> None: """ Send notification through appropriate channels based on user preferences. Args: notification: The notification to send """ user = notification.user # Get user's notification preferences try: preferences = user.notification_preference except NotificationPreference.DoesNotExist: # Create default preferences if they don't exist preferences = NotificationPreference.objects.create(user=user) # Send email notification if enabled if preferences.should_send_notification( notification.notification_type, "email" ): NotificationService._send_email_notification(notification) # Send push notification if enabled if preferences.should_send_notification(notification.notification_type, "push"): NotificationService._send_push_notification(notification) # In-app notifications are always created (the notification object itself) # The frontend will check preferences when displaying them @staticmethod def _send_email_notification(notification: UserNotification) -> None: """ Send email notification to user. Args: notification: The notification to send via email """ try: user = notification.user # Prepare email context context = { "user": user, "notification": notification, "site_name": "ThrillWiki", "site_url": getattr(settings, "SITE_URL", "https://thrillwiki.com"), } # Render email templates subject = f"ThrillWiki: {notification.title}" html_message = render_to_string("emails/notification.html", context) plain_message = render_to_string("emails/notification.txt", context) # Send email send_mail( subject=subject, message=plain_message, html_message=html_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[user.email], fail_silently=False, ) # Mark as sent notification.email_sent = True notification.email_sent_at = timezone.now() notification.save(update_fields=["email_sent", "email_sent_at"]) logger.info( f"Email notification sent to {user.email} for notification {notification.id}" ) except Exception as e: logger.error( f"Failed to send email notification {notification.id}: {str(e)}" ) @staticmethod def _send_push_notification(notification: UserNotification) -> None: """ Send push notification to user. Args: notification: The notification to send via push """ try: # TODO: Implement push notification service (Firebase, etc.) # For now, just mark as sent notification.push_sent = True notification.push_sent_at = timezone.now() notification.save(update_fields=["push_sent", "push_sent_at"]) logger.info(f"Push notification sent for notification {notification.id}") except Exception as e: logger.error( f"Failed to send push notification {notification.id}: {str(e)}" ) @staticmethod def get_user_notifications( user: User, unread_only: bool = False, notification_types: Optional[List[str]] = None, limit: Optional[int] = None, ) -> List[UserNotification]: """ Get notifications for a user. Args: user: User to get notifications for unread_only: Only return unread notifications notification_types: Filter by notification types limit: Limit number of results Returns: List[UserNotification]: List of notifications """ queryset = UserNotification.objects.filter(user=user) if unread_only: queryset = queryset.filter(is_read=False) if notification_types: queryset = queryset.filter(notification_type__in=notification_types) # Exclude expired notifications queryset = queryset.filter( models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now()) ) if limit: queryset = queryset[:limit] return list(queryset) @staticmethod def mark_notifications_read( user: User, notification_ids: Optional[List[int]] = None ) -> int: """ Mark notifications as read for a user. Args: user: User whose notifications to mark as read notification_ids: Specific notification IDs to mark as read (if None, marks all) Returns: int: Number of notifications marked as read """ queryset = UserNotification.objects.filter(user=user, is_read=False) if notification_ids: queryset = queryset.filter(id__in=notification_ids) return queryset.update(is_read=True, read_at=timezone.now()) @staticmethod def cleanup_old_notifications(days: int = 90) -> int: """ Clean up old read notifications. Args: days: Number of days to keep read notifications Returns: int: Number of notifications deleted """ cutoff_date = timezone.now() - timezone.timedelta(days=days) old_notifications = UserNotification.objects.filter( is_read=True, read_at__lt=cutoff_date ) count = old_notifications.count() old_notifications.delete() logger.info(f"Cleaned up {count} old notifications") return count