mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 22:07:03 -05:00
428 lines
14 KiB
Python
428 lines
14 KiB
Python
"""
|
|
Signal handlers for moderation-related FSM state transitions.
|
|
|
|
This module provides signal handlers that execute when moderation
|
|
models (EditSubmission, PhotoSubmission, ModerationReport, etc.)
|
|
undergo state transitions.
|
|
|
|
Includes:
|
|
- Transition handlers for approval, rejection, escalation
|
|
- Real-time broadcasting signal for dashboard updates
|
|
- Claim/unclaim tracking for concurrency control
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.dispatch import Signal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Custom Signals for Real-Time Broadcasting
|
|
# ============================================================================
|
|
|
|
# Signal emitted when a submission status changes - for real-time UI updates
|
|
# Arguments:
|
|
# - sender: The model class (EditSubmission or PhotoSubmission)
|
|
# - submission_id: The ID of the submission
|
|
# - submission_type: "edit" or "photo"
|
|
# - new_status: The new status value
|
|
# - previous_status: The previous status value
|
|
# - locked_by: Username of the moderator who claimed it (or None)
|
|
# - payload: Full payload dictionary for broadcasting
|
|
submission_status_changed = Signal()
|
|
|
|
|
|
def handle_submission_claimed(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle submission claim transitions.
|
|
|
|
Called when an EditSubmission or PhotoSubmission is claimed by a moderator.
|
|
Broadcasts the status change for real-time dashboard updates.
|
|
|
|
Args:
|
|
instance: The submission instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who claimed.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "CLAIMED":
|
|
return
|
|
|
|
logger.info(f"Submission {instance.pk} claimed by {user.username if user else 'system'}")
|
|
|
|
# Broadcast for real-time dashboard updates
|
|
_broadcast_submission_status_change(instance, source, target, user)
|
|
|
|
|
|
def handle_submission_unclaimed(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle submission unclaim transitions (CLAIMED -> PENDING).
|
|
|
|
Called when a moderator releases their claim on a submission.
|
|
|
|
Args:
|
|
instance: The submission instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who unclaimed.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if source != "CLAIMED" or target != "PENDING":
|
|
return
|
|
|
|
logger.info(f"Submission {instance.pk} unclaimed by {user.username if user else 'system'}")
|
|
|
|
# Broadcast for real-time dashboard updates
|
|
_broadcast_submission_status_change(instance, source, target, user)
|
|
|
|
|
|
def handle_submission_approved(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle submission approval transitions.
|
|
|
|
Called when an EditSubmission or PhotoSubmission is approved.
|
|
|
|
Args:
|
|
instance: The submission instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who approved.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "APPROVED":
|
|
return
|
|
|
|
logger.info(f"Submission {instance.pk} approved by {user if user else 'system'}")
|
|
|
|
# Trigger notification (handled by NotificationCallback)
|
|
# Invalidate cache (handled by CacheInvalidationCallback)
|
|
|
|
# Apply the submission changes if applicable
|
|
if hasattr(instance, "apply_changes"):
|
|
try:
|
|
instance.apply_changes()
|
|
logger.info(f"Applied changes for submission {instance.pk}")
|
|
except Exception as e:
|
|
logger.exception(f"Failed to apply changes for submission {instance.pk}: {e}")
|
|
|
|
|
|
def handle_submission_rejected(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle submission rejection transitions.
|
|
|
|
Called when an EditSubmission or PhotoSubmission is rejected.
|
|
|
|
Args:
|
|
instance: The submission instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who rejected.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "REJECTED":
|
|
return
|
|
|
|
reason = context.extra_data.get("reason", "") if context else ""
|
|
logger.info(
|
|
f"Submission {instance.pk} rejected by {user if user else 'system'}" f"{f': {reason}' if reason else ''}"
|
|
)
|
|
|
|
|
|
def handle_submission_escalated(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle submission escalation transitions.
|
|
|
|
Called when an EditSubmission or PhotoSubmission is escalated.
|
|
|
|
Args:
|
|
instance: The submission instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who escalated.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "ESCALATED":
|
|
return
|
|
|
|
reason = context.extra_data.get("reason", "") if context else ""
|
|
logger.info(
|
|
f"Submission {instance.pk} escalated by {user if user else 'system'}" f"{f': {reason}' if reason else ''}"
|
|
)
|
|
|
|
# Create escalation task if task system is available
|
|
_create_escalation_task(instance, user, reason)
|
|
|
|
|
|
def handle_report_resolved(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle moderation report resolution.
|
|
|
|
Called when a ModerationReport is resolved.
|
|
|
|
Args:
|
|
instance: The ModerationReport instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who resolved.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "RESOLVED":
|
|
return
|
|
|
|
logger.info(f"ModerationReport {instance.pk} resolved by {user if user else 'system'}")
|
|
|
|
# Update related queue items
|
|
_update_related_queue_items(instance, "COMPLETED")
|
|
|
|
|
|
def handle_queue_completed(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle moderation queue completion.
|
|
|
|
Called when a ModerationQueue item is completed.
|
|
|
|
Args:
|
|
instance: The ModerationQueue instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who completed.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
if target != "COMPLETED":
|
|
return
|
|
|
|
logger.info(f"ModerationQueue {instance.pk} completed by {user if user else 'system'}")
|
|
|
|
# Update moderation statistics
|
|
_update_moderation_stats(instance, user)
|
|
|
|
|
|
def handle_bulk_operation_status(instance, source, target, user, context=None, **kwargs):
|
|
"""
|
|
Handle bulk operation status changes.
|
|
|
|
Called when a BulkOperation transitions between states.
|
|
|
|
Args:
|
|
instance: The BulkOperation instance.
|
|
source: The source state.
|
|
target: The target state.
|
|
user: The user who initiated the change.
|
|
context: Optional TransitionContext.
|
|
"""
|
|
logger.info(f"BulkOperation {instance.pk} transitioned: {source} → {target}")
|
|
|
|
if target == "COMPLETED":
|
|
_finalize_bulk_operation(instance, success=True)
|
|
elif target == "FAILED":
|
|
_finalize_bulk_operation(instance, success=False)
|
|
|
|
|
|
# Helper functions
|
|
|
|
|
|
def _create_escalation_task(instance, user, reason):
|
|
"""Create an escalation task for admin review."""
|
|
try:
|
|
from apps.moderation.models import ModerationQueue
|
|
|
|
# Create a queue item for the escalated submission
|
|
ModerationQueue.objects.create(
|
|
content_object=instance,
|
|
priority="HIGH",
|
|
reason=f"Escalated: {reason}" if reason else "Escalated for review",
|
|
created_by=user,
|
|
)
|
|
logger.info(f"Created escalation queue item for submission {instance.pk}")
|
|
except ImportError:
|
|
logger.debug("ModerationQueue model not available")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to create escalation task: {e}")
|
|
|
|
|
|
def _update_related_queue_items(instance, status):
|
|
"""Update queue items related to a moderation object."""
|
|
try:
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
from apps.moderation.models import ModerationQueue
|
|
|
|
content_type = ContentType.objects.get_for_model(type(instance))
|
|
queue_items = ModerationQueue.objects.filter(
|
|
content_type=content_type,
|
|
object_id=instance.pk,
|
|
).exclude(status=status)
|
|
|
|
updated = queue_items.update(status=status)
|
|
if updated:
|
|
logger.info(f"Updated {updated} queue items to {status}")
|
|
except ImportError:
|
|
logger.debug("ModerationQueue model not available")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to update queue items: {e}")
|
|
|
|
|
|
def _update_moderation_stats(instance, user):
|
|
"""Update moderation statistics for a user."""
|
|
if not user:
|
|
return
|
|
|
|
try:
|
|
# Update user's moderation count if they have a profile
|
|
profile = getattr(user, "profile", None)
|
|
if profile and hasattr(profile, "moderation_count"):
|
|
profile.moderation_count += 1
|
|
profile.save(update_fields=["moderation_count"])
|
|
logger.debug(f"Updated moderation count for {user}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to update moderation stats: {e}")
|
|
|
|
|
|
def _finalize_bulk_operation(instance, success):
|
|
"""Finalize a bulk operation after completion or failure."""
|
|
try:
|
|
from django.utils import timezone
|
|
|
|
instance.completed_at = timezone.now()
|
|
instance.save(update_fields=["completed_at"])
|
|
|
|
if success:
|
|
logger.info(
|
|
f"BulkOperation {instance.pk} completed successfully: "
|
|
f"{getattr(instance, 'success_count', 0)} succeeded, "
|
|
f"{getattr(instance, 'failure_count', 0)} failed"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"BulkOperation {instance.pk} failed: " f"{getattr(instance, 'error_message', 'Unknown error')}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to finalize bulk operation: {e}")
|
|
|
|
|
|
def _broadcast_submission_status_change(instance, source, target, user):
|
|
"""
|
|
Broadcast submission status change for real-time UI updates.
|
|
|
|
Emits the submission_status_changed signal with a structured payload
|
|
that can be consumed by notification systems (Novu, SSE, WebSocket, etc.).
|
|
|
|
Payload format:
|
|
{
|
|
"submission_id": 123,
|
|
"submission_type": "edit" | "photo",
|
|
"new_status": "CLAIMED",
|
|
"previous_status": "PENDING",
|
|
"locked_by": "moderator_username" | None,
|
|
"locked_at": "2024-01-01T12:00:00Z" | None,
|
|
"changed_by": "username" | None,
|
|
}
|
|
"""
|
|
try:
|
|
from .models import EditSubmission
|
|
|
|
# Determine submission type
|
|
submission_type = "edit" if isinstance(instance, EditSubmission) else "photo"
|
|
|
|
# Build the broadcast payload
|
|
payload = {
|
|
"submission_id": instance.pk,
|
|
"submission_type": submission_type,
|
|
"new_status": target,
|
|
"previous_status": source,
|
|
"locked_by": None,
|
|
"locked_at": None,
|
|
"changed_by": user.username if user else None,
|
|
}
|
|
|
|
# Add claim information if available
|
|
if hasattr(instance, "claimed_by") and instance.claimed_by:
|
|
payload["locked_by"] = instance.claimed_by.username
|
|
if hasattr(instance, "claimed_at") and instance.claimed_at:
|
|
payload["locked_at"] = instance.claimed_at.isoformat()
|
|
|
|
# Emit the signal for downstream notification handlers
|
|
submission_status_changed.send(
|
|
sender=type(instance),
|
|
submission_id=instance.pk,
|
|
submission_type=submission_type,
|
|
new_status=target,
|
|
previous_status=source,
|
|
locked_by=payload["locked_by"],
|
|
payload=payload,
|
|
)
|
|
|
|
logger.debug(f"Broadcast status change: {submission_type}#{instance.pk} " f"{source} -> {target}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to broadcast submission status change: {e}")
|
|
|
|
|
|
# Signal handler registration
|
|
|
|
|
|
def register_moderation_signal_handlers():
|
|
"""
|
|
Register all moderation signal handlers.
|
|
|
|
This function should be called in the moderation app's AppConfig.ready() method.
|
|
"""
|
|
from apps.core.state_machine.signals import register_transition_handler
|
|
|
|
try:
|
|
from apps.moderation.models import (
|
|
BulkOperation,
|
|
EditSubmission,
|
|
ModerationQueue,
|
|
ModerationReport,
|
|
PhotoSubmission,
|
|
)
|
|
|
|
# EditSubmission handlers
|
|
register_transition_handler(EditSubmission, "*", "APPROVED", handle_submission_approved, stage="post")
|
|
register_transition_handler(EditSubmission, "*", "REJECTED", handle_submission_rejected, stage="post")
|
|
register_transition_handler(EditSubmission, "*", "ESCALATED", handle_submission_escalated, stage="post")
|
|
|
|
# PhotoSubmission handlers
|
|
register_transition_handler(PhotoSubmission, "*", "APPROVED", handle_submission_approved, stage="post")
|
|
register_transition_handler(PhotoSubmission, "*", "REJECTED", handle_submission_rejected, stage="post")
|
|
register_transition_handler(PhotoSubmission, "*", "ESCALATED", handle_submission_escalated, stage="post")
|
|
|
|
# ModerationReport handlers
|
|
register_transition_handler(ModerationReport, "*", "RESOLVED", handle_report_resolved, stage="post")
|
|
|
|
# ModerationQueue handlers
|
|
register_transition_handler(ModerationQueue, "*", "COMPLETED", handle_queue_completed, stage="post")
|
|
|
|
# BulkOperation handlers
|
|
register_transition_handler(BulkOperation, "*", "*", handle_bulk_operation_status, stage="post")
|
|
|
|
# Claim/Unclaim handlers for EditSubmission
|
|
register_transition_handler(EditSubmission, "PENDING", "CLAIMED", handle_submission_claimed, stage="post")
|
|
register_transition_handler(EditSubmission, "CLAIMED", "PENDING", handle_submission_unclaimed, stage="post")
|
|
|
|
# Claim/Unclaim handlers for PhotoSubmission
|
|
register_transition_handler(PhotoSubmission, "PENDING", "CLAIMED", handle_submission_claimed, stage="post")
|
|
register_transition_handler(PhotoSubmission, "CLAIMED", "PENDING", handle_submission_unclaimed, stage="post")
|
|
|
|
logger.info("Registered moderation signal handlers")
|
|
|
|
except ImportError as e:
|
|
logger.warning(f"Could not register moderation signal handlers: {e}")
|
|
|
|
|
|
__all__ = [
|
|
"submission_status_changed",
|
|
"register_moderation_signal_handlers",
|
|
"handle_submission_approved",
|
|
"handle_submission_rejected",
|
|
"handle_submission_escalated",
|
|
"handle_submission_claimed",
|
|
"handle_submission_unclaimed",
|
|
"handle_report_resolved",
|
|
"handle_queue_completed",
|
|
"handle_bulk_operation_status",
|
|
]
|