Files
thrilltrack-explorer/django/apps/moderation/tasks.py
pacnpal d6ff4cc3a3 Add email templates for user notifications and account management
- 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.
2025-11-08 15:34:04 -05:00

305 lines
9.9 KiB
Python

"""
Background tasks for moderation workflows 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
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_moderation_notification(self, submission_id, status):
"""
Send email notification when a submission is approved or rejected.
Args:
submission_id: UUID of the ContentSubmission
status: 'approved' or 'rejected'
Returns:
str: Notification result message
"""
from apps.moderation.models import ContentSubmission
try:
submission = ContentSubmission.objects.select_related(
'user', 'reviewed_by', 'entity_type'
).prefetch_related('items').get(id=submission_id)
# Get user's submission count
user_submission_count = ContentSubmission.objects.filter(
user=submission.user
).count()
# Prepare email context
context = {
'submission': submission,
'status': status,
'user': submission.user,
'user_submission_count': user_submission_count,
'submission_url': f"{settings.SITE_URL}/submissions/{submission.id}/",
'site_url': settings.SITE_URL,
}
# Choose template based on status
if status == 'approved':
template = 'emails/moderation_approved.html'
subject = f'✅ Submission Approved: {submission.title}'
else:
template = 'emails/moderation_rejected.html'
subject = f'⚠️ Submission Requires Changes: {submission.title}'
# Render HTML email
html_message = render_to_string(template, context)
# Send email
send_mail(
subject=subject,
message='', # Plain text version (optional)
html_message=html_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[submission.user.email],
fail_silently=False,
)
logger.info(
f"Moderation notification sent: {status} for submission {submission_id} "
f"to {submission.user.email}"
)
return f"Notification sent to {submission.user.email}"
except ContentSubmission.DoesNotExist:
logger.error(f"Submission {submission_id} not found")
raise
except Exception as exc:
logger.error(f"Error sending notification for submission {submission_id}: {str(exc)}")
# Retry with exponential backoff
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@shared_task(bind=True, max_retries=2)
def cleanup_expired_locks(self):
"""
Clean up expired moderation locks.
This task runs periodically to unlock submissions that have
been locked for too long (default: 15 minutes).
Returns:
int: Number of locks cleaned up
"""
from apps.moderation.models import ModerationLock
try:
cleaned = ModerationLock.cleanup_expired()
logger.info(f"Cleaned up {cleaned} expired moderation locks")
return cleaned
except Exception as exc:
logger.error(f"Error cleaning up expired locks: {str(exc)}")
raise self.retry(exc=exc, countdown=300) # Retry after 5 minutes
@shared_task(bind=True, max_retries=3)
def send_batch_moderation_summary(self, moderator_id):
"""
Send a daily summary email to a moderator with their moderation stats.
Args:
moderator_id: ID of the moderator user
Returns:
str: Email send result
"""
from apps.users.models import User
from apps.moderation.models import ContentSubmission
from datetime import timedelta
try:
moderator = User.objects.get(id=moderator_id)
# Get stats for the past 24 hours
yesterday = timezone.now() - timedelta(days=1)
stats = {
'reviewed_today': ContentSubmission.objects.filter(
reviewed_by=moderator,
reviewed_at__gte=yesterday
).count(),
'approved_today': ContentSubmission.objects.filter(
reviewed_by=moderator,
reviewed_at__gte=yesterday,
status='approved'
).count(),
'rejected_today': ContentSubmission.objects.filter(
reviewed_by=moderator,
reviewed_at__gte=yesterday,
status='rejected'
).count(),
'pending_queue': ContentSubmission.objects.filter(
status='pending'
).count(),
}
context = {
'moderator': moderator,
'stats': stats,
'date': timezone.now(),
'site_url': settings.SITE_URL,
}
# For now, just log the stats (template not created yet)
logger.info(f"Moderation summary for {moderator.email}: {stats}")
# In production, you would send an actual email:
# html_message = render_to_string('emails/moderation_summary.html', context)
# send_mail(...)
return f"Summary sent to {moderator.email}"
except User.DoesNotExist:
logger.error(f"Moderator {moderator_id} not found")
raise
except Exception as exc:
logger.error(f"Error sending moderation summary: {str(exc)}")
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@shared_task
def update_moderation_statistics():
"""
Update moderation-related statistics across the database.
Returns:
dict: Updated statistics
"""
from apps.moderation.models import ContentSubmission
from django.db.models import Count, Avg, F
from datetime import timedelta
try:
now = timezone.now()
week_ago = now - timedelta(days=7)
stats = {
'total_submissions': ContentSubmission.objects.count(),
'pending': ContentSubmission.objects.filter(status='pending').count(),
'reviewing': ContentSubmission.objects.filter(status='reviewing').count(),
'approved': ContentSubmission.objects.filter(status='approved').count(),
'rejected': ContentSubmission.objects.filter(status='rejected').count(),
'this_week': ContentSubmission.objects.filter(
created_at__gte=week_ago
).count(),
'by_type': dict(
ContentSubmission.objects.values('submission_type')
.annotate(count=Count('id'))
.values_list('submission_type', 'count')
),
}
logger.info(f"Moderation statistics updated: {stats}")
return stats
except Exception as e:
logger.error(f"Error updating moderation statistics: {str(e)}")
raise
@shared_task(bind=True, max_retries=2)
def auto_unlock_stale_reviews(self, hours=1):
"""
Automatically unlock submissions that have been in review for too long.
This helps prevent submissions from getting stuck if a moderator
starts a review but doesn't complete it.
Args:
hours: Number of hours before auto-unlocking (default: 1)
Returns:
int: Number of submissions unlocked
"""
from apps.moderation.models import ContentSubmission
from apps.moderation.services import ModerationService
from datetime import timedelta
try:
cutoff = timezone.now() - timedelta(hours=hours)
# Find submissions that have been reviewing too long
stale_reviews = ContentSubmission.objects.filter(
status='reviewing',
locked_at__lt=cutoff
)
count = 0
for submission in stale_reviews:
try:
ModerationService.unlock_submission(submission.id)
count += 1
except Exception as e:
logger.error(f"Failed to unlock submission {submission.id}: {str(e)}")
continue
logger.info(f"Auto-unlocked {count} stale reviews")
return count
except Exception as exc:
logger.error(f"Error auto-unlocking stale reviews: {str(exc)}")
raise self.retry(exc=exc, countdown=300)
@shared_task
def notify_moderators_of_queue_size():
"""
Notify moderators when the pending queue gets too large.
This helps ensure timely review of submissions.
Returns:
dict: Notification result
"""
from apps.moderation.models import ContentSubmission
from apps.users.models import User
try:
pending_count = ContentSubmission.objects.filter(status='pending').count()
# Threshold for notification (configurable)
threshold = getattr(settings, 'MODERATION_QUEUE_THRESHOLD', 50)
if pending_count >= threshold:
# Get all moderators
moderators = User.objects.filter(role__is_moderator=True)
logger.warning(
f"Moderation queue size ({pending_count}) exceeds threshold ({threshold}). "
f"Notifying {moderators.count()} moderators."
)
# In production, send emails to moderators
# For now, just log
return {
'queue_size': pending_count,
'threshold': threshold,
'notified': moderators.count(),
}
else:
logger.info(f"Moderation queue size ({pending_count}) is within threshold")
return {
'queue_size': pending_count,
'threshold': threshold,
'notified': 0,
}
except Exception as e:
logger.error(f"Error checking moderation queue: {str(e)}")
raise