mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
352 lines
12 KiB
Python
352 lines
12 KiB
Python
"""
|
|
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
|