mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
344 lines
10 KiB
Python
344 lines
10 KiB
Python
"""
|
|
Background tasks for user management and notifications.
|
|
"""
|
|
|
|
import logging
|
|
from celery import shared_task
|
|
from django.core.mail import send_mail
|
|
from django.template.loader import render_to_string
|
|
from django.conf import settings
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
|
def send_welcome_email(self, user_id):
|
|
"""
|
|
Send a welcome email to a newly registered user.
|
|
|
|
Args:
|
|
user_id: ID of the User
|
|
|
|
Returns:
|
|
str: Email send result
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
|
|
context = {
|
|
'user': user,
|
|
'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'),
|
|
}
|
|
|
|
html_message = render_to_string('emails/welcome.html', context)
|
|
|
|
send_mail(
|
|
subject='Welcome to ThrillWiki! 🎢',
|
|
message='',
|
|
html_message=html_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[user.email],
|
|
fail_silently=False,
|
|
)
|
|
|
|
logger.info(f"Welcome email sent to {user.email}")
|
|
return f"Welcome email sent to {user.email}"
|
|
|
|
except User.DoesNotExist:
|
|
logger.error(f"User {user_id} not found")
|
|
raise
|
|
except Exception as exc:
|
|
logger.error(f"Error sending welcome email to user {user_id}: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
|
|
|
|
|
|
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
|
def send_password_reset_email(self, user_id, token, reset_url):
|
|
"""
|
|
Send a password reset email with a secure token.
|
|
|
|
Args:
|
|
user_id: ID of the User
|
|
token: Password reset token
|
|
reset_url: Full URL for password reset
|
|
|
|
Returns:
|
|
str: Email send result
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
|
|
context = {
|
|
'user': user,
|
|
'reset_url': reset_url,
|
|
'request_time': timezone.now(),
|
|
'expiry_hours': 24, # Configurable
|
|
'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'),
|
|
}
|
|
|
|
html_message = render_to_string('emails/password_reset.html', context)
|
|
|
|
send_mail(
|
|
subject='Reset Your ThrillWiki Password',
|
|
message='',
|
|
html_message=html_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[user.email],
|
|
fail_silently=False,
|
|
)
|
|
|
|
logger.info(f"Password reset email sent to {user.email}")
|
|
return f"Password reset email sent to {user.email}"
|
|
|
|
except User.DoesNotExist:
|
|
logger.error(f"User {user_id} not found")
|
|
raise
|
|
except Exception as exc:
|
|
logger.error(f"Error sending password reset email: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
|
|
|
|
|
|
@shared_task(bind=True, max_retries=2)
|
|
def cleanup_expired_tokens(self):
|
|
"""
|
|
Clean up expired JWT tokens and password reset tokens.
|
|
|
|
This task runs daily to remove old tokens from the database.
|
|
|
|
Returns:
|
|
dict: Cleanup statistics
|
|
"""
|
|
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
|
|
from django.contrib.auth.tokens import default_token_generator
|
|
|
|
try:
|
|
# Clean up blacklisted JWT tokens older than 7 days
|
|
cutoff = timezone.now() - timedelta(days=7)
|
|
|
|
# Note: Actual implementation depends on token storage strategy
|
|
# This is a placeholder for the concept
|
|
|
|
logger.info("Token cleanup completed")
|
|
|
|
return {
|
|
'jwt_tokens_cleaned': 0,
|
|
'reset_tokens_cleaned': 0,
|
|
}
|
|
|
|
except Exception as exc:
|
|
logger.error(f"Error cleaning up tokens: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=300)
|
|
|
|
|
|
@shared_task(bind=True, max_retries=3)
|
|
def send_account_notification(self, user_id, notification_type, context_data=None):
|
|
"""
|
|
Send a generic account notification email.
|
|
|
|
Args:
|
|
user_id: ID of the User
|
|
notification_type: Type of notification (e.g., 'security_alert', 'profile_update')
|
|
context_data: Additional context data for the email
|
|
|
|
Returns:
|
|
str: Email send result
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
|
|
context = {
|
|
'user': user,
|
|
'notification_type': notification_type,
|
|
'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'),
|
|
}
|
|
|
|
if context_data:
|
|
context.update(context_data)
|
|
|
|
# For now, just log (would need specific templates for each type)
|
|
logger.info(f"Account notification ({notification_type}) for user {user.email}")
|
|
|
|
return f"Notification sent to {user.email}"
|
|
|
|
except User.DoesNotExist:
|
|
logger.error(f"User {user_id} not found")
|
|
raise
|
|
except Exception as exc:
|
|
logger.error(f"Error sending account notification: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
|
|
|
|
|
|
@shared_task(bind=True, max_retries=2)
|
|
def cleanup_inactive_users(self, days_inactive=365):
|
|
"""
|
|
Clean up or flag users who haven't logged in for a long time.
|
|
|
|
Args:
|
|
days_inactive: Number of days of inactivity before flagging (default: 365)
|
|
|
|
Returns:
|
|
dict: Cleanup statistics
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
cutoff = timezone.now() - timedelta(days=days_inactive)
|
|
|
|
inactive_users = User.objects.filter(
|
|
last_login__lt=cutoff,
|
|
is_active=True
|
|
)
|
|
|
|
count = inactive_users.count()
|
|
|
|
# For now, just log inactive users
|
|
# In production, you might want to send reactivation emails
|
|
# or mark accounts for deletion
|
|
|
|
logger.info(f"Found {count} inactive users (last login before {cutoff})")
|
|
|
|
return {
|
|
'inactive_count': count,
|
|
'cutoff_date': cutoff.isoformat(),
|
|
}
|
|
|
|
except Exception as exc:
|
|
logger.error(f"Error cleaning up inactive users: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=300)
|
|
|
|
|
|
@shared_task
|
|
def update_user_statistics():
|
|
"""
|
|
Update user-related statistics across the database.
|
|
|
|
Returns:
|
|
dict: Updated statistics
|
|
"""
|
|
from apps.users.models import User
|
|
from django.db.models import Count
|
|
from datetime import timedelta
|
|
|
|
try:
|
|
now = timezone.now()
|
|
week_ago = now - timedelta(days=7)
|
|
month_ago = now - timedelta(days=30)
|
|
|
|
stats = {
|
|
'total_users': User.objects.count(),
|
|
'active_users': User.objects.filter(is_active=True).count(),
|
|
'new_this_week': User.objects.filter(date_joined__gte=week_ago).count(),
|
|
'new_this_month': User.objects.filter(date_joined__gte=month_ago).count(),
|
|
'verified_users': User.objects.filter(email_verified=True).count(),
|
|
'by_role': dict(
|
|
User.objects.values('role__name')
|
|
.annotate(count=Count('id'))
|
|
.values_list('role__name', 'count')
|
|
),
|
|
}
|
|
|
|
logger.info(f"User statistics updated: {stats}")
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating user statistics: {str(e)}")
|
|
raise
|
|
|
|
|
|
@shared_task(bind=True, max_retries=3)
|
|
def send_bulk_notification(self, user_ids, subject, message, html_message=None):
|
|
"""
|
|
Send bulk email notifications to multiple users.
|
|
|
|
This is useful for announcements, feature updates, etc.
|
|
|
|
Args:
|
|
user_ids: List of User IDs
|
|
subject: Email subject
|
|
message: Plain text message
|
|
html_message: HTML version of message (optional)
|
|
|
|
Returns:
|
|
dict: Send statistics
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
users = User.objects.filter(id__in=user_ids, is_active=True)
|
|
|
|
sent_count = 0
|
|
failed_count = 0
|
|
|
|
for user in users:
|
|
try:
|
|
send_mail(
|
|
subject=subject,
|
|
message=message,
|
|
html_message=html_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[user.email],
|
|
fail_silently=False,
|
|
)
|
|
sent_count += 1
|
|
except Exception as e:
|
|
logger.error(f"Failed to send to {user.email}: {str(e)}")
|
|
failed_count += 1
|
|
continue
|
|
|
|
result = {
|
|
'total': len(user_ids),
|
|
'sent': sent_count,
|
|
'failed': failed_count,
|
|
}
|
|
|
|
logger.info(f"Bulk notification sent: {result}")
|
|
return result
|
|
|
|
except Exception as exc:
|
|
logger.error(f"Error sending bulk notification: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
|
|
|
|
|
|
@shared_task(bind=True, max_retries=2)
|
|
def send_email_verification_reminder(self, user_id):
|
|
"""
|
|
Send a reminder to users who haven't verified their email.
|
|
|
|
Args:
|
|
user_id: ID of the User
|
|
|
|
Returns:
|
|
str: Reminder result
|
|
"""
|
|
from apps.users.models import User
|
|
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
|
|
if user.email_verified:
|
|
logger.info(f"User {user.email} already verified, skipping reminder")
|
|
return "User already verified"
|
|
|
|
# Send verification reminder
|
|
logger.info(f"Sending email verification reminder to {user.email}")
|
|
|
|
# In production, generate new verification token and send email
|
|
# For now, just log
|
|
|
|
return f"Verification reminder sent to {user.email}"
|
|
|
|
except User.DoesNotExist:
|
|
logger.error(f"User {user_id} not found")
|
|
raise
|
|
except Exception as exc:
|
|
logger.error(f"Error sending verification reminder: {str(exc)}")
|
|
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
|