mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 13:31:18 -05:00
feat(state-machine): add comprehensive callback system for transitions
Extend state machine module with callback infrastructure including: - Pre/post/error transition callbacks with registry - Signal-based transition notifications - Callback configuration and monitoring support - Helper functions for callback registration - Improved park ride count updates with FSM integration
This commit is contained in:
@@ -1,13 +1,24 @@
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModerationConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.moderation"
|
||||
verbose_name = "Content Moderation"
|
||||
|
||||
def ready(self):
|
||||
"""Initialize state machines for all moderation models."""
|
||||
"""Initialize state machines and callbacks for all moderation models."""
|
||||
self._apply_state_machines()
|
||||
self._register_callbacks()
|
||||
self._register_signal_handlers()
|
||||
|
||||
def _apply_state_machines(self):
|
||||
"""Apply FSM to all moderation models."""
|
||||
from apps.core.state_machine import apply_state_machine
|
||||
from .models import (
|
||||
EditSubmission,
|
||||
@@ -48,3 +59,113 @@ class ModerationConfig(AppConfig):
|
||||
choice_group="photo_submission_statuses",
|
||||
domain="moderation",
|
||||
)
|
||||
|
||||
def _register_callbacks(self):
|
||||
"""Register FSM transition callbacks for moderation models."""
|
||||
from apps.core.state_machine.registry import register_callback
|
||||
from apps.core.state_machine.callbacks.notifications import (
|
||||
SubmissionApprovedNotification,
|
||||
SubmissionRejectedNotification,
|
||||
SubmissionEscalatedNotification,
|
||||
ModerationNotificationCallback,
|
||||
)
|
||||
from apps.core.state_machine.callbacks.cache import (
|
||||
ModerationCacheInvalidation,
|
||||
)
|
||||
from .models import (
|
||||
EditSubmission,
|
||||
ModerationReport,
|
||||
ModerationQueue,
|
||||
BulkOperation,
|
||||
PhotoSubmission,
|
||||
)
|
||||
|
||||
# EditSubmission callbacks
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'APPROVED',
|
||||
SubmissionApprovedNotification()
|
||||
)
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'APPROVED',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'REJECTED',
|
||||
SubmissionRejectedNotification()
|
||||
)
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'REJECTED',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'ESCALATED',
|
||||
SubmissionEscalatedNotification()
|
||||
)
|
||||
register_callback(
|
||||
EditSubmission, 'status', 'PENDING', 'ESCALATED',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
|
||||
# PhotoSubmission callbacks
|
||||
register_callback(
|
||||
PhotoSubmission, 'status', 'PENDING', 'APPROVED',
|
||||
SubmissionApprovedNotification()
|
||||
)
|
||||
register_callback(
|
||||
PhotoSubmission, 'status', 'PENDING', 'APPROVED',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
register_callback(
|
||||
PhotoSubmission, 'status', 'PENDING', 'REJECTED',
|
||||
SubmissionRejectedNotification()
|
||||
)
|
||||
register_callback(
|
||||
PhotoSubmission, 'status', 'PENDING', 'REJECTED',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
register_callback(
|
||||
PhotoSubmission, 'status', 'PENDING', 'ESCALATED',
|
||||
SubmissionEscalatedNotification()
|
||||
)
|
||||
|
||||
# ModerationReport callbacks
|
||||
register_callback(
|
||||
ModerationReport, 'status', '*', '*',
|
||||
ModerationNotificationCallback()
|
||||
)
|
||||
register_callback(
|
||||
ModerationReport, 'status', '*', '*',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
|
||||
# ModerationQueue callbacks
|
||||
register_callback(
|
||||
ModerationQueue, 'status', '*', '*',
|
||||
ModerationNotificationCallback()
|
||||
)
|
||||
register_callback(
|
||||
ModerationQueue, 'status', '*', '*',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
|
||||
# BulkOperation callbacks
|
||||
register_callback(
|
||||
BulkOperation, 'status', '*', '*',
|
||||
ModerationNotificationCallback()
|
||||
)
|
||||
register_callback(
|
||||
BulkOperation, 'status', '*', '*',
|
||||
ModerationCacheInvalidation()
|
||||
)
|
||||
|
||||
logger.debug("Registered moderation transition callbacks")
|
||||
|
||||
def _register_signal_handlers(self):
|
||||
"""Register signal handlers for moderation transitions."""
|
||||
from .signals import register_moderation_signal_handlers
|
||||
|
||||
try:
|
||||
register_moderation_signal_handlers()
|
||||
logger.debug("Registered moderation signal handlers")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not register moderation signal handlers: {e}")
|
||||
|
||||
@@ -9,6 +9,11 @@ This module contains models for the ThrillWiki moderation system, including:
|
||||
- BulkOperation: Administrative bulk operations
|
||||
|
||||
All models use pghistory for change tracking and TrackedModel base class.
|
||||
|
||||
Callback System Integration:
|
||||
All FSM-enabled models in this module support the callback system.
|
||||
Callbacks for notifications, cache invalidation, and related updates
|
||||
are registered via the callback configuration defined in each model's Meta class.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional, Union
|
||||
@@ -29,6 +34,35 @@ from apps.core.state_machine import RichFSMField, StateMachineMixin
|
||||
UserType = Union[AbstractBaseUser, AnonymousUser]
|
||||
|
||||
|
||||
# Lazy callback imports to avoid circular dependencies
|
||||
def _get_notification_callbacks():
|
||||
"""Lazy import of notification callbacks."""
|
||||
from apps.core.state_machine.callbacks.notifications import (
|
||||
SubmissionApprovedNotification,
|
||||
SubmissionRejectedNotification,
|
||||
SubmissionEscalatedNotification,
|
||||
ModerationNotificationCallback,
|
||||
)
|
||||
return {
|
||||
'approved': SubmissionApprovedNotification,
|
||||
'rejected': SubmissionRejectedNotification,
|
||||
'escalated': SubmissionEscalatedNotification,
|
||||
'moderation': ModerationNotificationCallback,
|
||||
}
|
||||
|
||||
|
||||
def _get_cache_callbacks():
|
||||
"""Lazy import of cache callbacks."""
|
||||
from apps.core.state_machine.callbacks.cache import (
|
||||
CacheInvalidationCallback,
|
||||
ModerationCacheInvalidation,
|
||||
)
|
||||
return {
|
||||
'generic': CacheInvalidationCallback,
|
||||
'moderation': ModerationCacheInvalidation,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Original EditSubmission Model (Preserved)
|
||||
# ============================================================================
|
||||
|
||||
326
backend/apps/moderation/signals.py
Normal file
326
backend/apps/moderation/signals.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
|
||||
from apps.core.state_machine.signals import (
|
||||
post_state_transition,
|
||||
state_transition_failed,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
# 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 (
|
||||
EditSubmission,
|
||||
PhotoSubmission,
|
||||
ModerationReport,
|
||||
ModerationQueue,
|
||||
BulkOperation,
|
||||
)
|
||||
|
||||
# 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'
|
||||
)
|
||||
|
||||
logger.info("Registered moderation signal handlers")
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(f"Could not register moderation signal handlers: {e}")
|
||||
Reference in New Issue
Block a user