Files
thrillwiki_django_no_react/backend/apps/rides/models/rankings.py
pacnpal dcf890a55c feat: Implement Entity Suggestion Manager and Modal components
- Added EntitySuggestionManager.vue to manage entity suggestions and authentication.
- Created EntitySuggestionModal.vue for displaying suggestions and adding new entities.
- Integrated AuthManager for user authentication within the suggestion modal.
- Enhanced signal handling in start-servers.sh for graceful shutdown of servers.
- Improved server startup script to ensure proper cleanup and responsiveness to termination signals.
- Added documentation for signal handling fixes and usage instructions.
2025-08-25 10:46:54 -04:00

213 lines
7.1 KiB
Python

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