""" 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.conf import settings from django.db import models from typing import Optional, Dict, Any, List from datetime import datetime, timedelta import logging from apps.accounts.models import User, UserNotification, NotificationPreference from django_forwardemail.services import EmailService 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[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) # Toast notifications are always created (the notification object itself) # The frontend will display them as toast notifications based on preferences @staticmethod def _send_email_notification(notification: UserNotification) -> None: """ Send email notification to user using the custom ForwardEmail service. 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 using custom ForwardEmail service EmailService.send_email( to=user.email, subject=subject, text=plain_message, html=html_message, ) # 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 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() - 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