mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 12:11:14 -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,25 @@
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RidesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.rides"
|
||||
|
||||
def ready(self):
|
||||
import apps.rides.choices # noqa: F401 - Register choices
|
||||
import apps.rides.signals # noqa: F401 - Register signals
|
||||
import apps.rides.tasks # noqa: F401 - Register Celery tasks
|
||||
|
||||
self._apply_state_machines()
|
||||
self._register_callbacks()
|
||||
|
||||
def _apply_state_machines(self):
|
||||
"""Apply FSM to ride models."""
|
||||
from apps.core.state_machine import apply_state_machine
|
||||
from apps.rides.models import Ride
|
||||
|
||||
@@ -15,3 +27,58 @@ class RidesConfig(AppConfig):
|
||||
apply_state_machine(
|
||||
Ride, field_name="status", choice_group="statuses", domain="rides"
|
||||
)
|
||||
|
||||
def _register_callbacks(self):
|
||||
"""Register FSM transition callbacks for ride models."""
|
||||
from apps.core.state_machine.registry import register_callback
|
||||
from apps.core.state_machine.callbacks.cache import (
|
||||
RideCacheInvalidation,
|
||||
APICacheInvalidation,
|
||||
)
|
||||
from apps.core.state_machine.callbacks.related_updates import (
|
||||
ParkCountUpdateCallback,
|
||||
SearchTextUpdateCallback,
|
||||
)
|
||||
from apps.rides.models import Ride
|
||||
|
||||
# Cache invalidation for all ride status changes
|
||||
register_callback(
|
||||
Ride, 'status', '*', '*',
|
||||
RideCacheInvalidation()
|
||||
)
|
||||
|
||||
# API cache invalidation
|
||||
register_callback(
|
||||
Ride, 'status', '*', '*',
|
||||
APICacheInvalidation(include_geo_cache=True)
|
||||
)
|
||||
|
||||
# Park count updates for status changes that affect active rides
|
||||
register_callback(
|
||||
Ride, 'status', '*', 'OPERATING',
|
||||
ParkCountUpdateCallback()
|
||||
)
|
||||
register_callback(
|
||||
Ride, 'status', 'OPERATING', '*',
|
||||
ParkCountUpdateCallback()
|
||||
)
|
||||
register_callback(
|
||||
Ride, 'status', '*', 'CLOSED_PERM',
|
||||
ParkCountUpdateCallback()
|
||||
)
|
||||
register_callback(
|
||||
Ride, 'status', '*', 'DEMOLISHED',
|
||||
ParkCountUpdateCallback()
|
||||
)
|
||||
register_callback(
|
||||
Ride, 'status', '*', 'RELOCATED',
|
||||
ParkCountUpdateCallback()
|
||||
)
|
||||
|
||||
# Search text update
|
||||
register_callback(
|
||||
Ride, 'status', '*', '*',
|
||||
SearchTextUpdateCallback()
|
||||
)
|
||||
|
||||
logger.debug("Registered ride transition callbacks")
|
||||
|
||||
@@ -1,17 +1,188 @@
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Ride
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ride)
|
||||
def handle_ride_status(sender, instance, **kwargs):
|
||||
"""Handle ride status changes based on closing date"""
|
||||
if instance.closing_date:
|
||||
today = timezone.now().date()
|
||||
"""
|
||||
Handle ride status changes based on closing date.
|
||||
|
||||
# If we've reached the closing date and status is "Closing"
|
||||
if today >= instance.closing_date and instance.status == "CLOSING":
|
||||
# Change to the selected post-closing status
|
||||
instance.status = instance.post_closing_status or "SBNO"
|
||||
Integrates with FSM transitions by using transition methods when available.
|
||||
"""
|
||||
if not instance.closing_date:
|
||||
return
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
# If we've reached the closing date and status is "CLOSING"
|
||||
if today >= instance.closing_date and instance.status == "CLOSING":
|
||||
target_status = instance.post_closing_status or "SBNO"
|
||||
|
||||
logger.info(
|
||||
f"Ride {instance.pk} closing date reached, "
|
||||
f"transitioning to {target_status}"
|
||||
)
|
||||
|
||||
# Try to use FSM transition method if available
|
||||
transition_method_name = f'transition_to_{target_status.lower()}'
|
||||
if hasattr(instance, transition_method_name):
|
||||
# Check if transition is allowed before attempting
|
||||
if hasattr(instance, 'can_proceed'):
|
||||
can_proceed = getattr(instance, f'can_transition_to_{target_status.lower()}', None)
|
||||
if can_proceed and callable(can_proceed):
|
||||
if not can_proceed():
|
||||
logger.warning(
|
||||
f"FSM transition to {target_status} not allowed "
|
||||
f"for ride {instance.pk}"
|
||||
)
|
||||
# Fall back to direct status change
|
||||
instance.status = target_status
|
||||
instance.status_since = instance.closing_date
|
||||
return
|
||||
|
||||
try:
|
||||
method = getattr(instance, transition_method_name)
|
||||
method()
|
||||
instance.status_since = instance.closing_date
|
||||
logger.info(
|
||||
f"Applied FSM transition to {target_status} for ride {instance.pk}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Failed to apply FSM transition for ride {instance.pk}: {e}"
|
||||
)
|
||||
# Fall back to direct status change
|
||||
instance.status = target_status
|
||||
instance.status_since = instance.closing_date
|
||||
else:
|
||||
# No FSM transition method, use direct assignment
|
||||
instance.status = target_status
|
||||
instance.status_since = instance.closing_date
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ride)
|
||||
def validate_closing_status(sender, instance, **kwargs):
|
||||
"""
|
||||
Validate that post_closing_status is set when entering CLOSING state.
|
||||
"""
|
||||
# Only validate if this is an existing ride being updated
|
||||
if not instance.pk:
|
||||
return
|
||||
|
||||
# Check if we're transitioning to CLOSING
|
||||
if instance.status == "CLOSING":
|
||||
# Ensure post_closing_status is set
|
||||
if not instance.post_closing_status:
|
||||
logger.warning(
|
||||
f"Ride {instance.pk} entering CLOSING without post_closing_status set"
|
||||
)
|
||||
# Default to SBNO if not set
|
||||
instance.post_closing_status = "SBNO"
|
||||
|
||||
# Ensure closing_date is set
|
||||
if not instance.closing_date:
|
||||
logger.warning(
|
||||
f"Ride {instance.pk} entering CLOSING without closing_date set"
|
||||
)
|
||||
# Default to today's date
|
||||
instance.closing_date = timezone.now().date()
|
||||
|
||||
|
||||
# FSM transition signal handlers
|
||||
|
||||
def handle_ride_transition_to_closing(instance, source, target, user, **kwargs):
|
||||
"""
|
||||
Validate transition to CLOSING status.
|
||||
|
||||
This function is called by the FSM callback system before a ride
|
||||
transitions to CLOSING status.
|
||||
|
||||
Args:
|
||||
instance: The Ride instance.
|
||||
source: The source state.
|
||||
target: The target state.
|
||||
user: The user who initiated the transition.
|
||||
|
||||
Returns:
|
||||
True if transition should proceed, False to abort.
|
||||
"""
|
||||
if target != 'CLOSING':
|
||||
return True
|
||||
|
||||
if not instance.post_closing_status:
|
||||
logger.error(
|
||||
f"Cannot transition ride {instance.pk} to CLOSING: "
|
||||
"post_closing_status not set"
|
||||
)
|
||||
return False
|
||||
|
||||
if not instance.closing_date:
|
||||
logger.warning(
|
||||
f"Ride {instance.pk} transitioning to CLOSING without closing_date"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def apply_post_closing_status(instance, user=None):
|
||||
"""
|
||||
Apply the post_closing_status to a ride in CLOSING state.
|
||||
|
||||
This function can be called by the FSM callback system or directly
|
||||
when a ride's closing date is reached.
|
||||
|
||||
Args:
|
||||
instance: The Ride instance in CLOSING state.
|
||||
user: The user initiating the change (optional).
|
||||
|
||||
Returns:
|
||||
True if status was applied, False otherwise.
|
||||
"""
|
||||
if instance.status != 'CLOSING':
|
||||
logger.debug(
|
||||
f"Ride {instance.pk} not in CLOSING state, skipping"
|
||||
)
|
||||
return False
|
||||
|
||||
target_status = instance.post_closing_status
|
||||
if not target_status:
|
||||
logger.warning(
|
||||
f"Ride {instance.pk} in CLOSING but no post_closing_status set"
|
||||
)
|
||||
return False
|
||||
|
||||
# Try to use FSM transition
|
||||
transition_method_name = f'transition_to_{target_status.lower()}'
|
||||
if hasattr(instance, transition_method_name):
|
||||
try:
|
||||
method = getattr(instance, transition_method_name)
|
||||
method(user=user)
|
||||
instance.post_closing_status = None
|
||||
instance.save(update_fields=['post_closing_status'])
|
||||
logger.info(
|
||||
f"Applied post_closing_status {target_status} to ride {instance.pk}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Failed to apply post_closing_status for ride {instance.pk}: {e}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
# Direct status change
|
||||
instance.status = target_status
|
||||
instance.post_closing_status = None
|
||||
instance.status_since = timezone.now().date()
|
||||
instance.save(update_fields=['status', 'post_closing_status', 'status_since'])
|
||||
logger.info(
|
||||
f"Applied post_closing_status {target_status} to ride {instance.pk} (direct)"
|
||||
)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user