Files
thrillwiki_django_no_react/backend/apps/rides/signals.py
pacnpal 2e35f8c5d9 feat: Refactor rides app with unique constraints, mixins, and enhanced documentation
- Added migration to convert unique_together constraints to UniqueConstraint for RideModel.
- Introduced RideFormMixin for handling entity suggestions in ride forms.
- Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements.
- Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling.
- Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples.
- Implemented a benchmarking script for query performance analysis and optimization.
- Developed security documentation detailing measures, configurations, and a security checklist.
- Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
2025-12-22 11:17:31 -05:00

266 lines
8.9 KiB
Python

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}")