""" 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