import logging from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from django.utils import timezone from .models import Ride logger = logging.getLogger(__name__) # ============================================================================= # Computed Field Maintenance # ============================================================================= def update_ride_search_text(ride): """ Update ride's search_text computed field. This is called when related objects (park, manufacturer, ride_model) change and might affect the ride's search text. """ if ride is None: return try: ride._populate_computed_fields() ride.save(update_fields=['search_text']) logger.debug(f"Updated search_text for ride {ride.pk}") except Exception as e: logger.exception(f"Failed to update search_text for ride {ride.pk}: {e}") @receiver(pre_save, sender=Ride) def handle_ride_status(sender, instance, **kwargs): """ Handle ride status changes based on closing date. 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 # ============================================================================= # Computed Field Maintenance Signal Handlers # ============================================================================= @receiver(post_save, sender='parks.Park') def update_ride_search_text_on_park_change(sender, instance, **kwargs): """ Update ride search_text when park name or location changes. When a park's name changes, all rides at that park need their search_text regenerated. """ try: for ride in instance.rides.all(): update_ride_search_text(ride) except Exception as e: logger.exception(f"Failed to update ride search_text on park change: {e}") @receiver(post_save, sender='parks.Company') def update_ride_search_text_on_company_change(sender, instance, **kwargs): """ Update ride search_text when manufacturer/designer name changes. When a company's name changes, all rides manufactured or designed by that company need their search_text regenerated. """ try: # Update all rides manufactured by this company for ride in instance.manufactured_rides.all(): update_ride_search_text(ride) # Update all rides designed by this company for ride in instance.designed_rides.all(): update_ride_search_text(ride) except Exception as e: logger.exception(f"Failed to update ride search_text on company change: {e}") @receiver(post_save, sender='rides.RideModel') def update_ride_search_text_on_ride_model_change(sender, instance, **kwargs): """ Update ride search_text when ride model name changes. When a ride model's name changes, all rides using that model need their search_text regenerated. """ try: for ride in instance.rides.all(): update_ride_search_text(ride) except Exception as e: logger.exception(f"Failed to update ride search_text on ride model change: {e}")