from django.db import models from django.urls import reverse from django.utils.text import slugify from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet from history_tracking.signals import get_current_branch, ChangesetContextManager # Shared choices that will be used by multiple models CATEGORY_CHOICES = [ ('', 'Select ride type'), ('RC', 'Roller Coaster'), ('DR', 'Dark Ride'), ('FR', 'Flat Ride'), ('WR', 'Water Ride'), ('TR', 'Transport'), ('OT', 'Other'), ] class RideModel(HistoricalModel): """ Represents a specific model/type of ride that can be manufactured by different companies. For example: B&M Dive Coaster, Vekoma Boomerang, etc. """ name = models.CharField(max_length=255) manufacturer = models.ForeignKey( 'companies.Manufacturer', on_delete=models.SET_NULL, # Changed to SET_NULL since it's optional related_name='ride_models', null=True, # Made optional blank=True # Made optional ) description = models.TextField(blank=True) category = models.CharField( max_length=2, choices=CATEGORY_CHOICES, default='', blank=True ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) comments = GenericRelation('comments.CommentThread', related_name='ride_model_threads', related_query_name='comments_thread' ) class Meta: ordering = ['manufacturer', 'name'] unique_together = ['manufacturer', 'name'] def __str__(self) -> str: return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}" def save(self, *args, **kwargs) -> None: # Get the branch from context or use default current_branch = get_current_branch() if current_branch: # Save in the context of the current branch super().save(*args, **kwargs) else: # If no branch context, save in main branch main_branch, _ = VersionBranch.objects.get_or_create( name='main', defaults={'metadata': {'type': 'default_branch'}} ) with ChangesetContextManager(branch=main_branch): super().save(*args, **kwargs) def get_version_info(self) -> dict: """Get version control information for this ride model""" content_type = ContentType.objects.get_for_model(self) latest_changes = ChangeSet.objects.filter( content_type=content_type, object_id=self.pk, status='applied' ).order_by('-created_at')[:5] active_branches = VersionBranch.objects.filter( changesets__content_type=content_type, changesets__object_id=self.pk, is_active=True ).distinct() return { 'latest_changes': latest_changes, 'active_branches': active_branches, 'current_branch': get_current_branch(), 'total_changes': latest_changes.count() } def get_absolute_url(self) -> str: return reverse("rides:model_detail", kwargs={"pk": self.pk}) class Ride(HistoricalModel): STATUS_CHOICES = [ ('OPERATING', 'Operating'), ('SBNO', 'Standing But Not Operating'), ('CLOSING', 'Closing'), ('CLOSED_PERM', 'Permanently Closed'), ('UNDER_CONSTRUCTION', 'Under Construction'), ('DEMOLISHED', 'Demolished'), ('RELOCATED', 'Relocated'), ] POST_CLOSING_STATUS_CHOICES = [ ('SBNO', 'Standing But Not Operating'), ('CLOSED_PERM', 'Permanently Closed'), ] name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) description = models.TextField(blank=True) park = models.ForeignKey( 'parks.Park', on_delete=models.CASCADE, related_name='rides' ) park_area = models.ForeignKey( 'parks.ParkArea', on_delete=models.SET_NULL, related_name='rides', null=True, blank=True ) category = models.CharField( max_length=2, choices=CATEGORY_CHOICES, default='', blank=True ) manufacturer = models.ForeignKey( 'companies.Manufacturer', on_delete=models.CASCADE, null=True, blank=True ) designer = models.ForeignKey( 'companies.Designer', on_delete=models.SET_NULL, related_name='rides', null=True, blank=True ) ride_model = models.ForeignKey( 'RideModel', on_delete=models.SET_NULL, related_name='rides', null=True, blank=True, help_text="The specific model/type of this ride" ) status = models.CharField( max_length=20, choices=STATUS_CHOICES, default='OPERATING' ) post_closing_status = models.CharField( max_length=20, choices=POST_CLOSING_STATUS_CHOICES, null=True, blank=True, help_text="Status to change to after closing date" ) opening_date = models.DateField(null=True, blank=True) closing_date = models.DateField(null=True, blank=True) status_since = models.DateField(null=True, blank=True) min_height_in = models.PositiveIntegerField(null=True, blank=True) max_height_in = models.PositiveIntegerField(null=True, blank=True) capacity_per_hour = models.PositiveIntegerField(null=True, blank=True) ride_duration_seconds = models.PositiveIntegerField(null=True, blank=True) average_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) photos = GenericRelation('media.Photo') reviews = GenericRelation('reviews.Review') comments = GenericRelation('comments.CommentThread', related_name='ride_threads', related_query_name='comments_thread' ) class Meta: ordering = ['name'] unique_together = ['park', 'slug'] def __str__(self) -> str: return f"{self.name} at {self.park.name}" def save(self, *args, **kwargs) -> None: if not self.slug: self.slug = slugify(self.name) # Get the branch from context or use default current_branch = get_current_branch() if current_branch: # Save in the context of the current branch super().save(*args, **kwargs) else: # If no branch context, save in main branch main_branch, _ = VersionBranch.objects.get_or_create( name='main', defaults={'metadata': {'type': 'default_branch'}} ) with ChangesetContextManager(branch=main_branch): super().save(*args, **kwargs) def get_version_info(self) -> dict: """Get version control information for this ride""" content_type = ContentType.objects.get_for_model(self) latest_changes = ChangeSet.objects.filter( content_type=content_type, object_id=self.pk, status='applied' ).order_by('-created_at')[:5] active_branches = VersionBranch.objects.filter( changesets__content_type=content_type, changesets__object_id=self.pk, is_active=True ).distinct() return { 'latest_changes': latest_changes, 'active_branches': active_branches, 'current_branch': get_current_branch(), 'total_changes': latest_changes.count(), 'parent_park_branch': self.park.get_version_info()['current_branch'] } def get_absolute_url(self) -> str: return reverse("rides:ride_detail", kwargs={ "park_slug": self.park.slug, "ride_slug": self.slug }) @classmethod def get_by_slug(cls, slug: str) -> tuple['Ride', bool]: """Get ride by current or historical slug""" try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: # Check historical slugs history = cls.history.filter(slug=slug).order_by("-history_date").first() if history: try: return cls.objects.get(pk=history.instance.pk), True except cls.DoesNotExist as e: raise cls.DoesNotExist("No ride found with this slug") from e raise cls.DoesNotExist("No ride found with this slug") class RollerCoasterStats(models.Model): TRACK_MATERIAL_CHOICES = [ ('STEEL', 'Steel'), ('WOOD', 'Wood'), ('HYBRID', 'Hybrid'), ] COASTER_TYPE_CHOICES = [ ('SITDOWN', 'Sit Down'), ('INVERTED', 'Inverted'), ('FLYING', 'Flying'), ('STANDUP', 'Stand Up'), ('WING', 'Wing'), ('DIVE', 'Dive'), ('FAMILY', 'Family'), ('WILD_MOUSE', 'Wild Mouse'), ('SPINNING', 'Spinning'), ('FOURTH_DIMENSION', '4th Dimension'), ('OTHER', 'Other'), ] LAUNCH_CHOICES = [ ('CHAIN', 'Chain Lift'), ('LSM', 'LSM Launch'), ('HYDRAULIC', 'Hydraulic Launch'), ('GRAVITY', 'Gravity'), ('OTHER', 'Other'), ] ride = models.OneToOneField( Ride, on_delete=models.CASCADE, related_name='coaster_stats' ) height_ft = models.DecimalField( max_digits=6, decimal_places=2, null=True, blank=True ) length_ft = models.DecimalField( max_digits=7, decimal_places=2, null=True, blank=True ) speed_mph = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True ) inversions = models.PositiveIntegerField(default=0) ride_time_seconds = models.PositiveIntegerField(null=True, blank=True) track_type = models.CharField(max_length=255, blank=True) track_material = models.CharField( max_length=20, choices=TRACK_MATERIAL_CHOICES, default='STEEL', blank=True ) roller_coaster_type = models.CharField( max_length=20, choices=COASTER_TYPE_CHOICES, default='SITDOWN', blank=True ) max_drop_height_ft = models.DecimalField( max_digits=6, decimal_places=2, null=True, blank=True ) launch_type = models.CharField( max_length=20, choices=LAUNCH_CHOICES, default='CHAIN' ) train_style = models.CharField(max_length=255, blank=True) trains_count = models.PositiveIntegerField(null=True, blank=True) cars_per_train = models.PositiveIntegerField(null=True, blank=True) seats_per_car = models.PositiveIntegerField(null=True, blank=True) class Meta: verbose_name = 'Roller Coaster Statistics' verbose_name_plural = 'Roller Coaster Statistics' def __str__(self) -> str: return f"Stats for {self.ride.name}"