mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -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.
305 lines
9.9 KiB
Python
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
|