""" Models for ride ranking system using Internet Roller Coaster Poll algorithm. This system calculates rankings based on pairwise comparisons between rides, where each ride is compared to every other ride to determine which one more riders preferred. """ from django.db import models from django.utils import timezone from django.core.validators import MinValueValidator, MaxValueValidator import pghistory @pghistory.track() class RideRanking(models.Model): """ Stores calculated rankings for rides using the Internet Roller Coaster Poll algorithm. Rankings are recalculated daily based on user reviews/ratings. Each ride's rank is determined by its winning percentage in pairwise comparisons. """ ride = models.OneToOneField( "rides.Ride", on_delete=models.CASCADE, related_name="ranking" ) # Core ranking metrics rank = models.PositiveIntegerField( db_index=True, help_text="Overall rank position (1 = best)" ) wins = models.PositiveIntegerField( default=0, help_text="Number of rides this ride beats in pairwise comparisons" ) losses = models.PositiveIntegerField( default=0, help_text="Number of rides that beat this ride in pairwise comparisons", ) ties = models.PositiveIntegerField( default=0, help_text="Number of rides with equal preference in pairwise comparisons", ) winning_percentage = models.DecimalField( max_digits=5, decimal_places=4, validators=[MinValueValidator(0), MaxValueValidator(1)], db_index=True, help_text="Win percentage where ties count as 0.5", ) # Additional metrics mutual_riders_count = models.PositiveIntegerField( default=0, help_text="Total number of users who have rated this ride" ) comparison_count = models.PositiveIntegerField( default=0, help_text="Number of other rides this was compared against" ) average_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(10)], help_text="Average rating from all users who have rated this ride", ) # Metadata last_calculated = models.DateTimeField( default=timezone.now, help_text="When this ranking was last calculated" ) calculation_version = models.CharField( max_length=10, default="1.0", help_text="Algorithm version used for calculation" ) class Meta: ordering = ["rank"] indexes = [ models.Index(fields=["rank"]), models.Index(fields=["winning_percentage", "-mutual_riders_count"]), models.Index(fields=["ride", "last_calculated"]), ] constraints = [ models.CheckConstraint( name="rideranking_winning_percentage_range", check=models.Q(winning_percentage__gte=0) & models.Q(winning_percentage__lte=1), violation_error_message="Winning percentage must be between 0 and 1", ), models.CheckConstraint( name="rideranking_average_rating_range", check=models.Q(average_rating__isnull=True) | (models.Q(average_rating__gte=1) & models.Q(average_rating__lte=10)), violation_error_message="Average rating must be between 1 and 10", ), ] def __str__(self): return f"#{self.rank} - {self.ride.name} ({self.winning_percentage:.1%})" @property def total_comparisons(self): """Total number of pairwise comparisons (wins + losses + ties).""" return self.wins + self.losses + self.ties @pghistory.track() class RidePairComparison(models.Model): """ Caches pairwise comparison results between two rides. This model stores the results of comparing two rides based on mutual riders (users who have rated both rides). It's used to speed up ranking calculations. """ ride_a = models.ForeignKey( "rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_a" ) ride_b = models.ForeignKey( "rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_b" ) # Comparison results ride_a_wins = models.PositiveIntegerField( default=0, help_text="Number of mutual riders who rated ride_a higher" ) ride_b_wins = models.PositiveIntegerField( default=0, help_text="Number of mutual riders who rated ride_b higher" ) ties = models.PositiveIntegerField( default=0, help_text="Number of mutual riders who rated both rides equally" ) # Metrics mutual_riders_count = models.PositiveIntegerField( default=0, help_text="Total number of users who have rated both rides" ) ride_a_avg_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True, help_text="Average rating of ride_a from mutual riders", ) ride_b_avg_rating = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True, help_text="Average rating of ride_b from mutual riders", ) # Metadata last_calculated = models.DateTimeField( auto_now=True, help_text="When this comparison was last calculated" ) class Meta: unique_together = [["ride_a", "ride_b"]] indexes = [ models.Index(fields=["ride_a", "ride_b"]), models.Index(fields=["last_calculated"]), ] def __str__(self): winner = "TIE" if self.ride_a_wins > self.ride_b_wins: winner = self.ride_a.name elif self.ride_b_wins > self.ride_a_wins: winner = self.ride_b.name return f"{self.ride_a.name} vs {self.ride_b.name} - Winner: {winner}" @property def winner(self): """Returns the winning ride or None for a tie.""" if self.ride_a_wins > self.ride_b_wins: return self.ride_a elif self.ride_b_wins > self.ride_a_wins: return self.ride_b return None @property def is_tie(self): """Returns True if the comparison resulted in a tie.""" return self.ride_a_wins == self.ride_b_wins class RankingSnapshot(models.Model): """ Stores historical snapshots of rankings for tracking changes over time. This allows us to show ranking trends and movements. """ ride = models.ForeignKey( "rides.Ride", on_delete=models.CASCADE, related_name="ranking_history" ) rank = models.PositiveIntegerField() winning_percentage = models.DecimalField(max_digits=5, decimal_places=4) snapshot_date = models.DateField( db_index=True, help_text="Date when this ranking snapshot was taken" ) class Meta: unique_together = [["ride", "snapshot_date"]] ordering = ["-snapshot_date", "rank"] indexes = [ models.Index(fields=["snapshot_date", "rank"]), models.Index(fields=["ride", "-snapshot_date"]), ] def __str__(self): return f"{self.ride.name} - Rank #{self.rank} on {self.snapshot_date}"