import logging from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.db.models import Q from apps.rides.models import Ride from .models import Park logger = logging.getLogger(__name__) # Status values that count as "active" rides for counting purposes ACTIVE_STATUSES = {'OPERATING', 'SEASONAL', 'UNDER_CONSTRUCTION'} # Status values that should decrement ride counts INACTIVE_STATUSES = {'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED', 'REMOVED'} def update_park_ride_counts(park, old_status=None, new_status=None): """ Update ride_count and coaster_count for a park. Args: park: The Park instance or park ID to update. old_status: The previous status of the ride (for FSM transitions). new_status: The new status of the ride (for FSM transitions). """ if park is None: logger.warning("Cannot update counts: park is None") return # Get park ID park_id = park.pk if hasattr(park, 'pk') else park try: # Fetch the park if we only have an ID if not hasattr(park, 'rides'): park = Park.objects.get(id=park_id) # Build the query for active rides active_statuses = list(ACTIVE_STATUSES) operating_rides = Q(status__in=active_statuses) # Count total operating rides ride_count = park.rides.filter(operating_rides).count() # Count total operating roller coasters coaster_count = park.rides.filter(operating_rides, category="RC").count() # Update park counts Park.objects.filter(id=park_id).update( ride_count=ride_count, coaster_count=coaster_count ) logger.debug( f"Updated park {park_id} counts: " f"ride_count={ride_count}, coaster_count={coaster_count}" ) except Park.DoesNotExist: logger.warning(f"Park {park_id} does not exist, cannot update counts") except Exception as e: logger.exception(f"Failed to update park counts for {park_id}: {e}") def should_update_counts(old_status, new_status): """ Determine if a status change should trigger count updates. Args: old_status: The previous status value. new_status: The new status value. Returns: True if counts should be updated, False otherwise. """ if old_status == new_status: return False # Check if either status is in active or inactive sets old_active = old_status in ACTIVE_STATUSES if old_status else False new_active = new_status in ACTIVE_STATUSES if new_status else False old_inactive = old_status in INACTIVE_STATUSES if old_status else False new_inactive = new_status in INACTIVE_STATUSES if new_status else False # Update if transitioning to/from active status return old_active != new_active or old_inactive != new_inactive @receiver(post_save, sender=Ride) def ride_saved(sender, instance, created, **kwargs): """ Update park counts when a ride is saved. Integrates with FSM transitions by checking for status changes. """ # For new rides, always update counts if created: update_park_ride_counts(instance.park) return # Check if status changed using model's tracker if available if hasattr(instance, 'tracker') and hasattr(instance.tracker, 'has_changed'): if instance.tracker.has_changed('status'): old_status = instance.tracker.previous('status') new_status = instance.status if should_update_counts(old_status, new_status): logger.info( f"Ride {instance.pk} status changed: {old_status} → {new_status}" ) update_park_ride_counts(instance.park, old_status, new_status) else: # Fallback: always update counts on save update_park_ride_counts(instance.park) @receiver(post_delete, sender=Ride) def ride_deleted(sender, instance, **kwargs): """ Update park counts when a ride is deleted. Logs the deletion for audit purposes. """ logger.info(f"Ride {instance.pk} deleted from park {instance.park_id}") update_park_ride_counts(instance.park) # FSM transition signal handlers def handle_ride_status_transition(instance, source, target, user, **kwargs): """ Handle ride status FSM transitions. This function is called by the FSM callback system when a ride status transition occurs. Args: instance: The Ride instance. source: The source state. target: The target state. user: The user who initiated the transition. """ if should_update_counts(source, target): logger.info( f"FSM transition: Ride {instance.pk} {source} → {target} " f"by {user if user else 'system'}" ) update_park_ride_counts(instance.park, source, target)