feat: Add blog, media, and support apps, implement ride credits and image API, and remove toplist feature.

This commit is contained in:
pacnpal
2025-12-26 15:15:28 -05:00
parent cd8868a591
commit 00699d53b4
77 changed files with 7274 additions and 538 deletions

View File

@@ -4,12 +4,17 @@ 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.conf import settings
from django.dispatch import receiver
from django.dispatch import receiver, Signal
from apps.core.state_machine.signals import (
post_state_transition,
@@ -20,6 +25,71 @@ from apps.core.state_machine.signals import (
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.
@@ -255,6 +325,66 @@ def _finalize_bulk_operation(instance, success):
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, PhotoSubmission
# 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():
@@ -320,7 +450,41 @@ def register_moderation_signal_handlers():
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',
]