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.
This commit is contained in:
pacnpal
2025-08-25 10:46:54 -04:00
parent 937eee19e4
commit dcf890a55c
61 changed files with 10328 additions and 740 deletions

View File

@@ -0,0 +1,716 @@
# Ride Ranking System - Complete Implementation Documentation
## Table of Contents
1. [Overview](#overview)
2. [Backend Implementation](#backend-implementation)
3. [Frontend Implementation](#frontend-implementation)
4. [API Reference](#api-reference)
5. [Usage Examples](#usage-examples)
6. [Deployment & Maintenance](#deployment--maintenance)
## Overview
The ThrillWiki Ride Ranking System implements the Internet Roller Coaster Poll (IRCP) algorithm to provide fair, data-driven rankings of theme park rides based on user ratings. This document covers the complete implementation across both backend (Django) and frontend (Vue.js/TypeScript) components.
### Key Features
- **Pairwise Comparison Algorithm**: Compares every ride against every other ride based on mutual riders
- **Web Interface**: Browse rankings with filtering and detailed views
- **REST API**: Comprehensive API for programmatic access
- **Historical Tracking**: Track ranking changes over time
- **Statistical Analysis**: Head-to-head comparisons and win/loss records
## Backend Implementation
### Database Models
#### Location: `apps/rides/models/rankings.py`
```python
# Core ranking models
- RideRanking: Current ranking data for each ride
- RidePairComparison: Cached pairwise comparison results
- RankingSnapshot: Historical ranking data
```
#### Key Fields
**RideRanking Model**:
- `rank` (Integer): Overall ranking position
- `wins` (Integer): Number of head-to-head wins
- `losses` (Integer): Number of head-to-head losses
- `ties` (Integer): Number of tied comparisons
- `winning_percentage` (Decimal): Win percentage (ties count as 0.5)
- `mutual_riders_count` (Integer): Total users who rated this ride
- `average_rating` (Decimal): Average user rating
- `last_calculated` (DateTime): Timestamp of last calculation
### Service Layer
#### Location: `apps/rides/services/ranking_service.py`
The `RideRankingService` class implements the core ranking algorithm:
```python
class RideRankingService:
def update_all_rankings(category=None):
"""Main entry point for ranking calculation"""
def _calculate_pairwise_comparison(ride_a, ride_b):
"""Compare two rides based on mutual riders"""
def _calculate_rankings_from_comparisons():
"""Convert comparisons to rankings"""
def _apply_tiebreakers():
"""Resolve ties using head-to-head comparisons"""
```
### Django Views
#### Location: `apps/rides/views.py`
**Web Views**:
```python
class RideRankingsView(ListView):
"""Main rankings list page with filtering"""
template_name = 'rides/rankings.html'
paginate_by = 50
class RideRankingDetailView(DetailView):
"""Detailed ranking view for a specific ride"""
template_name = 'rides/ranking_detail.html'
```
**HTMX Endpoints**:
- `ranking_history_chart`: Returns chart data for ranking history
- `ranking_comparisons`: Returns head-to-head comparison data
### URL Configuration
#### Location: `apps/rides/urls.py`
```python
urlpatterns = [
path('rankings/', RideRankingsView.as_view(), name='ride-rankings'),
path('rankings/<slug:ride_slug>/', RideRankingDetailView.as_view(), name='ride-ranking-detail'),
path('rankings/<slug:ride_slug>/history-chart/', ranking_history_chart, name='ranking-history-chart'),
path('rankings/<slug:ride_slug>/comparisons/', ranking_comparisons, name='ranking-comparisons'),
]
```
### API Implementation
#### Serializers
**Location**: `apps/api/v1/serializers_rankings.py`
```python
class RideRankingSerializer(serializers.ModelSerializer):
"""Basic ranking data serialization"""
class RideRankingDetailSerializer(serializers.ModelSerializer):
"""Detailed ranking with relationships"""
class RankingSnapshotSerializer(serializers.ModelSerializer):
"""Historical ranking data"""
class RankingStatsSerializer(serializers.Serializer):
"""System-wide statistics"""
```
#### ViewSets
**Location**: `apps/api/v1/viewsets_rankings.py`
```python
class RideRankingViewSet(viewsets.ReadOnlyModelViewSet):
"""
REST API endpoint for ride rankings
Supports filtering, ordering, and custom actions
"""
@action(detail=True, methods=['get'])
def history(self, request, pk=None):
"""Get historical ranking data"""
@action(detail=True, methods=['get'])
def comparisons(self, request, pk=None):
"""Get head-to-head comparisons"""
@action(detail=False, methods=['get'])
def statistics(self, request):
"""Get system-wide statistics"""
class TriggerRankingCalculationView(APIView):
"""Admin endpoint to trigger manual calculation"""
```
#### API URLs
**Location**: `apps/api/v1/urls.py`
```python
router.register(r'rankings', RideRankingViewSet, basename='ranking')
urlpatterns = [
path('', include(router.urls)),
path('rankings/calculate/', TriggerRankingCalculationView.as_view()),
]
```
### Management Commands
#### Location: `apps/rides/management/commands/update_ride_rankings.py`
```bash
# Update all rankings
python manage.py update_ride_rankings
# Update specific category
python manage.py update_ride_rankings --category RC
```
### Admin Interface
#### Location: `apps/rides/admin.py`
```python
@admin.register(RideRanking)
class RideRankingAdmin(admin.ModelAdmin):
list_display = ['rank', 'ride', 'winning_percentage', 'wins', 'losses']
list_filter = ['ride__category', 'last_calculated']
search_fields = ['ride__name']
ordering = ['rank']
```
## Frontend Implementation
### TypeScript Type Definitions
#### Location: `frontend/src/types/index.ts`
```typescript
// Core ranking types
export interface RideRanking {
id: number
rank: number
ride: {
id: number
name: string
slug: string
park: {
id: number
name: string
slug: string
}
category: 'RC' | 'DR' | 'FR' | 'WR' | 'TR' | 'OT'
}
wins: number
losses: number
ties: number
winning_percentage: number
mutual_riders_count: number
comparison_count: number
average_rating: number
last_calculated: string
rank_change?: number
previous_rank?: number | null
}
export interface RideRankingDetail extends RideRanking {
ride: {
// Extended ride information
description?: string
manufacturer?: { id: number; name: string }
opening_date?: string
status: string
}
calculation_version?: string
head_to_head_comparisons?: HeadToHeadComparison[]
ranking_history?: RankingSnapshot[]
}
export interface HeadToHeadComparison {
opponent: {
id: number
name: string
slug: string
park: string
}
wins: number
losses: number
ties: number
result: 'win' | 'loss' | 'tie'
mutual_riders: number
}
export interface RankingSnapshot {
date: string
rank: number
winning_percentage: number
}
export interface RankingStatistics {
total_ranked_rides: number
total_comparisons: number
last_calculation_time: string
calculation_duration: number
top_rated_ride?: RideInfo
most_compared_ride?: RideInfo
biggest_rank_change?: RankChangeInfo
}
```
### API Service Class
#### Location: `frontend/src/services/api.ts`
```typescript
export class RankingsApi {
// Core API methods
async getRankings(params?: RankingParams): Promise<ApiResponse<RideRanking>>
async getRankingDetail(rideSlug: string): Promise<RideRankingDetail>
async getRankingHistory(rideSlug: string): Promise<RankingSnapshot[]>
async getHeadToHeadComparisons(rideSlug: string): Promise<HeadToHeadComparison[]>
async getRankingStatistics(): Promise<RankingStatistics>
async calculateRankings(category?: string): Promise<CalculationResult>
// Convenience methods
async getTopRankings(limit: number, category?: string): Promise<RideRanking[]>
async getParkRankings(parkSlug: string, params?: Params): Promise<ApiResponse<RideRanking>>
async searchRankings(query: string): Promise<RideRanking[]>
async getRankChange(rideSlug: string): Promise<RankChangeInfo>
}
```
### Integration with Main API
```typescript
// Singleton instance with all API services
export const api = new ThrillWikiApi()
// Direct access to rankings API
export const rankingsApi = api.rankings
```
## API Reference
### REST Endpoints
#### Get Rankings List
```http
GET /api/v1/rankings/
```
**Query Parameters**:
- `page` (integer): Page number
- `page_size` (integer): Results per page (default: 20)
- `category` (string): Filter by category (RC, DR, FR, WR, TR, OT)
- `min_riders` (integer): Minimum mutual riders
- `park` (string): Filter by park slug
- `ordering` (string): Sort order (rank, -rank, winning_percentage, -winning_percentage)
**Response**:
```json
{
"count": 523,
"next": "http://api.example.com/api/v1/rankings/?page=2",
"previous": null,
"results": [
{
"id": 1,
"rank": 1,
"ride": {
"id": 123,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"park": {
"id": 45,
"name": "Cedar Point",
"slug": "cedar-point"
},
"category": "RC"
},
"wins": 487,
"losses": 23,
"ties": 13,
"winning_percentage": 0.9405,
"mutual_riders_count": 1543,
"comparison_count": 523,
"average_rating": 9.4,
"last_calculated": "2024-01-15T02:00:00Z"
}
]
}
```
#### Get Ranking Details
```http
GET /api/v1/rankings/{ride-slug}/
```
**Response**: Extended ranking data with full ride details, comparisons, and history
#### Get Ranking History
```http
GET /api/v1/rankings/{ride-slug}/history/
```
**Response**: Array of ranking snapshots (last 90 days)
#### Get Head-to-Head Comparisons
```http
GET /api/v1/rankings/{ride-slug}/comparisons/
```
**Response**: Array of comparison results with all other rides
#### Get Statistics
```http
GET /api/v1/rankings/statistics/
```
**Response**: System-wide ranking statistics
#### Trigger Calculation (Admin)
```http
POST /api/v1/rankings/calculate/
```
**Request Body**:
```json
{
"category": "RC" // Optional
}
```
**Response**:
```json
{
"status": "success",
"rides_ranked": 523,
"comparisons_made": 136503,
"duration": 45.23,
"timestamp": "2024-01-15T02:00:45Z"
}
```
## Usage Examples
### Frontend (Vue.js/TypeScript)
#### Display Top Rankings
```typescript
import { rankingsApi } from '@/services/api'
export default {
async mounted() {
try {
// Get top 10 rankings
const topRides = await rankingsApi.getTopRankings(10)
this.rankings = topRides
// Get roller coasters only
const response = await rankingsApi.getRankings({
category: 'RC',
page_size: 20,
ordering: 'rank'
})
this.rollerCoasters = response.results
} catch (error) {
console.error('Failed to load rankings:', error)
}
}
}
```
#### Display Ranking Details
```typescript
// In a Vue component
async loadRankingDetails(rideSlug: string) {
const [details, history, comparisons] = await Promise.all([
rankingsApi.getRankingDetail(rideSlug),
rankingsApi.getRankingHistory(rideSlug),
rankingsApi.getHeadToHeadComparisons(rideSlug)
])
this.rankingDetails = details
this.chartData = this.prepareChartData(history)
this.comparisons = comparisons
}
```
#### Search Rankings
```typescript
async searchRides(query: string) {
const results = await rankingsApi.searchRankings(query)
this.searchResults = results
}
```
### Backend (Python/Django)
#### Access Rankings in Views
```python
from apps.rides.models import RideRanking
# Get top 10 rides
top_rides = RideRanking.objects.select_related('ride', 'ride__park').order_by('rank')[:10]
# Get rankings for a specific category
coaster_rankings = RideRanking.objects.filter(
ride__category='RC'
).order_by('rank')
# Get ranking with change indicator
ranking = RideRanking.objects.get(ride__slug='millennium-force')
if ranking.previous_rank:
change = ranking.previous_rank - ranking.rank
direction = 'up' if change > 0 else 'down' if change < 0 else 'same'
```
#### Trigger Ranking Update
```python
from apps.rides.services.ranking_service import RideRankingService
# Update all rankings
service = RideRankingService()
result = service.update_all_rankings()
# Update specific category
result = service.update_all_rankings(category='RC')
print(f"Ranked {result['rides_ranked']} rides")
print(f"Made {result['comparisons_made']} comparisons")
print(f"Duration: {result['duration']:.2f} seconds")
```
### Command Line
```bash
# Update rankings via management command
uv run python manage.py update_ride_rankings
# Update only roller coasters
uv run python manage.py update_ride_rankings --category RC
# Schedule daily updates with cron
0 2 * * * cd /path/to/project && uv run python manage.py update_ride_rankings
```
## Deployment & Maintenance
### Initial Setup
1. **Run Migrations**:
```bash
uv run python manage.py migrate
```
2. **Initial Ranking Calculation**:
```bash
uv run python manage.py update_ride_rankings
```
3. **Verify in Admin**:
- Navigate to `/admin/rides/rideranking/`
- Verify rankings are populated
### Scheduled Updates
Add to crontab for daily updates:
```bash
# Update rankings daily at 2 AM
0 2 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings
# Optional: Update different categories at different times
0 2 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings --category RC
0 3 * * * cd /path/to/thrillwiki && uv run python manage.py update_ride_rankings --category DR
```
### Monitoring
1. **Check Logs**:
```bash
tail -f /path/to/logs/ranking_updates.log
```
2. **Monitor Performance**:
- Track calculation duration via API statistics endpoint
- Monitor database query performance
- Check comparison cache hit rates
3. **Data Validation**:
```python
# Check for ranking anomalies
from apps.rides.models import RideRanking
# Verify all ranks are unique
ranks = RideRanking.objects.values_list('rank', flat=True)
assert len(ranks) == len(set(ranks))
# Check winning percentage calculation
for ranking in RideRanking.objects.all():
expected = (ranking.wins + 0.5 * ranking.ties) / ranking.comparison_count
assert abs(ranking.winning_percentage - expected) < 0.001
```
### Performance Optimization
1. **Database Indexes**:
```sql
-- Ensure these indexes exist
CREATE INDEX idx_ranking_rank ON rides_rideranking(rank);
CREATE INDEX idx_ranking_ride ON rides_rideranking(ride_id);
CREATE INDEX idx_comparison_rides ON rides_ridepaircomparison(ride_a_id, ride_b_id);
```
2. **Cache Configuration**:
```python
# settings.py
CACHES = {
'rankings': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
'TIMEOUT': 3600, # 1 hour
}
}
```
3. **Batch Processing**:
- Process comparisons in batches of 1000
- Use bulk_create for database inserts
- Consider parallel processing for large datasets
### Troubleshooting
**Common Issues**:
1. **Rankings not updating**:
- Check cron job is running
- Verify database connectivity
- Check for lock files preventing concurrent runs
2. **Incorrect rankings**:
- Clear comparison cache and recalculate
- Verify rating data integrity
- Check for duplicate user ratings
3. **Performance issues**:
- Analyze slow queries with Django Debug Toolbar
- Consider increasing database resources
- Implement incremental updates for large datasets
### API Rate Limiting
Configure in `settings.py`:
```python
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour'
}
}
```
## Architecture Decisions
### Why Pairwise Comparison?
- **Fairness**: Only compares rides among users who've experienced both
- **Reduces Bias**: Popular rides aren't advantaged over less-ridden ones
- **Head-to-Head Logic**: Direct comparisons matter for tie-breaking
- **Robust to Outliers**: One extreme rating doesn't skew results
### Caching Strategy
- **Comparison Cache**: Store pairwise results to avoid recalculation
- **Snapshot History**: Keep 365 days of historical data
- **API Response Cache**: Cache ranking lists for 1 hour
### Scalability Considerations
- **O(n²) Complexity**: Scales quadratically with number of rides
- **Batch Processing**: Process in chunks to manage memory
- **Incremental Updates**: Future enhancement for real-time updates
## Testing
### Unit Tests
```python
# apps/rides/tests/test_ranking_service.py
class RankingServiceTestCase(TestCase):
def test_pairwise_comparison(self):
"""Test comparison logic between two rides"""
def test_ranking_calculation(self):
"""Test overall ranking calculation"""
def test_tiebreaker_logic(self):
"""Test head-to-head tiebreaker"""
```
### Integration Tests
```python
# apps/api/tests/test_ranking_api.py
class RankingAPITestCase(APITestCase):
def test_get_rankings_list(self):
"""Test ranking list endpoint"""
def test_filtering_and_ordering(self):
"""Test query parameters"""
def test_calculation_trigger(self):
"""Test admin calculation endpoint"""
```
### Frontend Tests
```typescript
// frontend/tests/api/rankings.test.ts
describe('Rankings API', () => {
it('should fetch top rankings', async () => {
const rankings = await rankingsApi.getTopRankings(10)
expect(rankings).toHaveLength(10)
expect(rankings[0].rank).toBe(1)
})
it('should filter by category', async () => {
const response = await rankingsApi.getRankings({ category: 'RC' })
expect(response.results.every(r => r.ride.category === 'RC')).toBe(true)
})
})
```
## Future Enhancements
### Planned Features
1. **Real-time Updates**: Update rankings immediately after new reviews
2. **Regional Rankings**: Rankings by geographic region
3. **Time-Period Rankings**: Best new rides, best classic rides
4. **User Preferences**: Personalized rankings based on user history
5. **Confidence Intervals**: Statistical confidence for rankings
6. **Mobile App API**: Optimized endpoints for mobile applications
### Potential Optimizations
1. **Incremental Updates**: Only recalculate affected comparisons
2. **Parallel Processing**: Distribute calculation across workers
3. **Machine Learning**: Predict rankings for new rides
4. **GraphQL API**: More flexible data fetching
5. **WebSocket Updates**: Real-time ranking changes
## Support & Documentation
### Additional Resources
- [Original IRCP Algorithm](https://ushsho.com/ridesurvey.py)
- [Django REST Framework Documentation](https://www.django-rest-framework.org/)
- [Vue.js Documentation](https://vuejs.org/)
- [TypeScript Documentation](https://www.typescriptlang.org/)
### Contact
For questions or issues related to the ranking system:
- Create an issue in the project repository
- Contact the development team
- Check the troubleshooting section above
---
*Last Updated: January 2025*
*Version: 1.0*

608
docs/ride-ranking-system.md Normal file
View File

@@ -0,0 +1,608 @@
# Ride Ranking System Documentation
## Overview
The ThrillWiki ride ranking system implements the **Internet Roller Coaster Poll (IRCP) algorithm** to calculate fair, accurate rankings for all rides based on user ratings. This system provides daily-updated rankings that reflect the collective preferences of the community.
## Algorithm Description
### Original Internet Roller Coaster Poll Algorithm
The system implements the exact methodology from the Internet Roller Coaster Poll:
> "Each coaster is compared one at a time to every other coaster to see whether more people who have ridden both of them preferred one or the other. A coaster is given a 'Win' for each coaster that more mutual riders ranked behind it, given a 'Loss' for each coaster that more mutual riders ranked ahead of it, and given a 'Tie' for each coaster that the same number of mutual riders ranked above it and below it. Coasters are ranked by their overall winning percentage (where ties count as half of a win and half of a loss). In the event that two coasters end up with identical winning percentages, the tie is broken (if possible) by determining which of the two won the mutual rider comparison between those two coasters."
### Our Implementation
The ThrillWiki implementation adapts this algorithm to work with our existing rating system:
1. **"Have You Ridden" Detection**: A user is considered to have ridden a ride if they have submitted a rating/review for it
2. **Preference Determination**: Higher ratings indicate preference (e.g., a user rating Ride A as 8/10 and Ride B as 6/10 prefers Ride A)
3. **Pairwise Comparisons**: Every ride is compared to every other ride based on mutual riders
4. **Winning Percentage**: Calculated as `(wins + 0.5 * ties) / total_comparisons`
5. **Tie Breaking**: Head-to-head comparisons resolve ties in winning percentage
## System Architecture
### Database Models
#### RideRanking
Stores the calculated ranking for each ride:
- `rank`: Overall rank position (1 = best)
- `wins`: Number of rides this ride beats in pairwise comparisons
- `losses`: Number of rides that beat this ride
- `ties`: Number of rides with equal preference
- `winning_percentage`: Win percentage where ties count as 0.5
- `mutual_riders_count`: Total users who have rated this ride
- `average_rating`: Average rating from all users
- `last_calculated`: Timestamp of last calculation
#### RidePairComparison
Caches pairwise comparison results between two rides:
- `ride_a`, `ride_b`: The two rides being compared
- `ride_a_wins`: Number of mutual riders who rated ride_a higher
- `ride_b_wins`: Number of mutual riders who rated ride_b higher
- `ties`: Number of mutual riders who rated both equally
- `mutual_riders_count`: Total users who rated both rides
#### RankingSnapshot
Historical tracking of rankings:
- `ride`: The ride being tracked
- `rank`: Rank on the snapshot date
- `winning_percentage`: Win percentage on the snapshot date
- `snapshot_date`: Date of the snapshot
### Service Layer
The `RideRankingService` (`apps/rides/services/ranking_service.py`) implements the core algorithm:
```python
service = RideRankingService()
result = service.update_all_rankings(category='RC') # Optional category filter
```
Key methods:
- `update_all_rankings()`: Main entry point for ranking calculation
- `_calculate_pairwise_comparison()`: Compares two rides
- `_calculate_rankings_from_comparisons()`: Converts comparisons to rankings
- `_apply_tiebreakers()`: Resolves ties using head-to-head comparisons
## Usage
### Manual Ranking Update
Run the management command to update rankings:
```bash
# Update all ride rankings
uv run python manage.py update_ride_rankings
# Update only roller coaster rankings
uv run python manage.py update_ride_rankings --category RC
```
### Scheduled Updates (Cron)
Add to crontab for daily updates at 2 AM:
```bash
0 2 * * * cd /Users/talor/thrillwiki_django_no_react/backend && uv run python manage.py update_ride_rankings
```
### Accessing Rankings
#### Django Admin
View rankings at `/admin/rides/rideranking/`
#### In Code
```python
from apps.rides.models import RideRanking
# Get top 10 rides
top_rides = RideRanking.objects.select_related('ride').order_by('rank')[:10]
# Get ranking for specific ride
ranking = RideRanking.objects.get(ride__slug='millennium-force')
print(f"Rank: #{ranking.rank}, Win%: {ranking.winning_percentage:.1%}")
```
## Web Interface
### Views
The ranking system provides comprehensive web views for browsing and analyzing rankings.
#### Rankings List View
**URL**: `/rides/rankings/`
**Features**:
- Paginated list of all ranked rides (50 per page)
- Filter by ride category (Roller Coasters, Dark Rides, etc.)
- Filter by minimum mutual riders to ensure statistical significance
- HTMX support for dynamic updates without page refresh
- Shows key metrics: rank, ride name, park, wins/losses/ties, winning percentage
**Query Parameters**:
- `category`: Filter by ride type (RC, DR, FR, WR, TR, OT)
- `min_riders`: Minimum number of mutual riders required (e.g., 100)
- `page`: Page number for pagination
**Example URLs**:
```
/rides/rankings/ # All rankings
/rides/rankings/?category=RC # Only roller coasters
/rides/rankings/?category=RC&min_riders=50 # Roller coasters with 50+ mutual riders
```
#### Ranking Detail View
**URL**: `/rides/rankings/<ride-slug>/`
**Features**:
- Comprehensive ranking metrics for a specific ride
- Head-to-head comparison results with other rides
- Historical ranking chart showing trends over time
- Rank movement indicators (up/down from previous calculation)
- Detailed breakdown of wins, losses, and ties
**HTMX Endpoints**:
- `/rides/rankings/<ride-slug>/history-chart/` - Returns ranking history chart data
- `/rides/rankings/<ride-slug>/comparisons/` - Returns detailed comparison table
### Template Integration
The views use Django templates with HTMX for dynamic updates:
```django
{# rankings.html - Main rankings page #}
{% extends "base.html" %}
{% block content %}
<div class="rankings-container">
<h1>Ride Rankings</h1>
{# Filters #}
<form hx-get="/rides/rankings/" hx-target="#rankings-table">
<select name="category">
<option value="all">All Rides</option>
<option value="RC">Roller Coasters</option>
<option value="DR">Dark Rides</option>
<!-- etc. -->
</select>
<input type="number" name="min_riders" placeholder="Min riders">
<button type="submit">Filter</button>
</form>
{# Rankings table #}
<div id="rankings-table">
{% include "rides/partials/rankings_table.html" %}
</div>
</div>
{% endblock %}
```
## API Endpoints
### RESTful API
The ranking system exposes a comprehensive REST API for programmatic access.
#### List Rankings
**Endpoint**: `GET /api/v1/rankings/`
**Description**: Get paginated list of ride rankings
**Query Parameters**:
- `category` (string): Filter by ride category (RC, DR, FR, WR, TR, OT)
- `min_riders` (integer): Minimum mutual riders required
- `park` (string): Filter by park slug
- `ordering` (string): Sort order (rank, -rank, winning_percentage, -winning_percentage)
- `page` (integer): Page number
- `page_size` (integer): Results per page (default: 20)
**Response Example**:
```json
{
"count": 523,
"next": "http://api.example.com/api/v1/rankings/?page=2",
"previous": null,
"results": [
{
"id": 1,
"rank": 1,
"ride": {
"id": 123,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"park": {
"id": 45,
"name": "Cedar Point",
"slug": "cedar-point"
},
"category": "RC"
},
"wins": 487,
"losses": 23,
"ties": 13,
"winning_percentage": 0.9405,
"mutual_riders_count": 1543,
"comparison_count": 523,
"average_rating": 9.4,
"last_calculated": "2024-01-15T02:00:00Z",
"rank_change": 0,
"previous_rank": 1
}
]
}
```
#### Get Ranking Details
**Endpoint**: `GET /api/v1/rankings/<ride-slug>/`
**Description**: Get detailed ranking information for a specific ride
**Response**: Includes full ride details, head-to-head comparisons, and ranking history
```json
{
"id": 1,
"rank": 1,
"ride": {
"id": 123,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"description": "Hybrid roller coaster...",
"park": {
"id": 45,
"name": "Cedar Point",
"slug": "cedar-point",
"location": {
"city": "Sandusky",
"state": "Ohio",
"country": "United States"
}
},
"category": "RC",
"manufacturer": {
"id": 12,
"name": "Rocky Mountain Construction"
},
"opening_date": "2018-05-05",
"status": "OPERATING"
},
"wins": 487,
"losses": 23,
"ties": 13,
"winning_percentage": 0.9405,
"mutual_riders_count": 1543,
"comparison_count": 523,
"average_rating": 9.4,
"last_calculated": "2024-01-15T02:00:00Z",
"calculation_version": "1.0",
"head_to_head_comparisons": [
{
"opponent": {
"id": 124,
"name": "Fury 325",
"slug": "fury-325",
"park": "Carowinds"
},
"wins": 234,
"losses": 189,
"ties": 23,
"result": "win",
"mutual_riders": 446
}
],
"ranking_history": [
{
"date": "2024-01-15",
"rank": 1,
"winning_percentage": 0.9405
},
{
"date": "2024-01-14",
"rank": 1,
"winning_percentage": 0.9398
}
]
}
```
#### Get Ranking History
**Endpoint**: `GET /api/v1/rankings/<ride-slug>/history/`
**Description**: Get historical ranking data for a specific ride (last 90 days)
**Response**: Array of ranking snapshots
#### Get Head-to-Head Comparisons
**Endpoint**: `GET /api/v1/rankings/<ride-slug>/comparisons/`
**Description**: Get detailed head-to-head comparison data for a ride
**Response**: Array of comparison results with all rides
#### Get Ranking Statistics
**Endpoint**: `GET /api/v1/rankings/statistics/`
**Description**: Get system-wide ranking statistics
**Response Example**:
```json
{
"total_ranked_rides": 523,
"total_comparisons": 136503,
"last_calculation_time": "2024-01-15T02:00:00Z",
"calculation_duration": 45.23,
"top_rated_ride": {
"id": 123,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"park": "Cedar Point",
"rank": 1,
"winning_percentage": 0.9405,
"average_rating": 9.4
},
"most_compared_ride": {
"id": 456,
"name": "Millennium Force",
"slug": "millennium-force",
"park": "Cedar Point",
"comparison_count": 521
},
"biggest_rank_change": {
"ride": {
"id": 789,
"name": "Iron Gwazi",
"slug": "iron-gwazi"
},
"current_rank": 3,
"previous_rank": 8,
"change": 5
}
}
```
#### Trigger Ranking Calculation (Admin)
**Endpoint**: `POST /api/v1/rankings/calculate/`
**Description**: Manually trigger ranking calculation (requires admin authentication)
**Request Body**:
```json
{
"category": "RC" // Optional - filter to specific category
}
```
**Response**:
```json
{
"status": "success",
"rides_ranked": 523,
"comparisons_made": 136503,
"duration": 45.23,
"timestamp": "2024-01-15T02:00:45Z"
}
```
### API Authentication
- Read endpoints (GET): No authentication required
- Calculation endpoint (POST): Requires admin authentication via token or session
### API Rate Limiting
- Anonymous users: 100 requests per hour
- Authenticated users: 1000 requests per hour
- Admin users: No rate limiting
### Python Client Example
```python
import requests
# Get top 10 rankings
response = requests.get(
"https://api.thrillwiki.com/api/v1/rankings/",
params={"page_size": 10}
)
rankings = response.json()["results"]
for ranking in rankings:
print(f"#{ranking['rank']}: {ranking['ride']['name']} - {ranking['winning_percentage']:.1%}")
# Get specific ride ranking
response = requests.get(
"https://api.thrillwiki.com/api/v1/rankings/steel-vengeance/"
)
details = response.json()
print(f"{details['ride']['name']} is ranked #{details['rank']}")
print(f"Wins: {details['wins']}, Losses: {details['losses']}, Ties: {details['ties']}")
```
### JavaScript/TypeScript Client Example
```typescript
// Using fetch API
async function getTopRankings(limit: number = 10) {
const response = await fetch(
`https://api.thrillwiki.com/api/v1/rankings/?page_size=${limit}`
);
const data = await response.json();
return data.results;
}
// Using axios
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.thrillwiki.com/api/v1',
});
async function getRideRanking(slug: string) {
const { data } = await api.get(`/rankings/${slug}/`);
return data;
}
```
## Calculation Example
### Scenario
Three rides with the following ratings from users:
**Ride A** (Millennium Force):
- User 1: 10/10
- User 2: 9/10
- User 3: 8/10
**Ride B** (Maverick):
- User 1: 9/10
- User 2: 10/10
- User 4: 8/10
**Ride C** (Top Thrill 2):
- User 1: 8/10
- User 3: 9/10
- User 4: 10/10
### Pairwise Comparisons
**A vs B** (mutual riders: Users 1, 2):
- User 1: A(10) > B(9) → A wins
- User 2: A(9) < B(10) → B wins
- Result: 1 win each, 0 ties
**A vs C** (mutual riders: Users 1, 3):
- User 1: A(10) > C(8) → A wins
- User 3: A(8) < C(9) → C wins
- Result: 1 win each, 0 ties
**B vs C** (mutual riders: Users 1, 4):
- User 1: B(9) > C(8) → B wins
- User 4: B(8) < C(10) → C wins
- Result: 1 win each, 0 ties
### Final Rankings
All three rides have:
- Wins: 1
- Losses: 1
- Ties: 0
- Winning Percentage: 50%
Since all have identical winning percentages, the system would use additional criteria (mutual rider count, average rating) to determine final order.
## Performance Considerations
### Optimization Strategies
1. **Caching**: Pairwise comparisons are cached in `RidePairComparison` table
2. **Batch Processing**: Comparisons calculated in bulk to minimize database queries
3. **Indexes**: Database indexes on frequently queried fields:
- `rank` for ordering
- `winning_percentage` for sorting
- `ride_id` for lookups
- Composite indexes for complex queries
### Scalability
The algorithm complexity is O(n²) where n is the number of rides, as each ride must be compared to every other ride. For 1,000 rides:
- Comparisons needed: 499,500
- Estimated processing time: 30-60 seconds
- Database storage: ~20MB for comparison cache
## Monitoring and Maintenance
### Admin Interface
Access Django admin to:
- View current rankings
- Inspect pairwise comparisons
- Track historical changes
- Monitor calculation performance
### Logging
The system logs:
- Calculation start/end times
- Number of rides ranked
- Number of comparisons made
- Any errors or warnings
Example log output:
```
INFO: Starting ranking calculation for category: RC
INFO: Found 523 rides to rank
DEBUG: Processed 100/136503 comparisons
INFO: Ranking calculation completed in 45.23 seconds
```
### Data Cleanup
Old ranking snapshots are automatically cleaned up after 365 days to manage database size.
## Algorithm Validity
### Mathematical Properties
1. **Transitivity**: Not enforced (A > B and B > C doesn't guarantee A > C)
2. **Consistency**: Same input always produces same output
3. **Fairness**: Every mutual rider's opinion counts equally
4. **Completeness**: All possible comparisons are considered
### Advantages Over Simple Averaging
1. **Reduces Selection Bias**: Only compares rides among users who've experienced both
2. **Fair Comparisons**: Popular rides aren't advantaged over less-ridden ones
3. **Head-to-Head Logic**: Direct comparisons matter for tie-breaking
4. **Robust to Outliers**: One extremely high/low rating doesn't skew results
## Future Enhancements
### Planned Features
1. **Real-time Updates**: Update rankings immediately after new reviews
2. **Category-Specific Rankings**: Separate rankings for different ride types
3. **Regional Rankings**: Rankings by geographic region
4. **Time-Period Rankings**: Best new rides, best classic rides
5. **API Endpoints**: RESTful API for ranking data
6. **Ranking Trends**: Visualizations of ranking changes over time
### Potential Optimizations
1. **Incremental Updates**: Only recalculate affected comparisons
2. **Parallel Processing**: Distribute comparison calculations
3. **Machine Learning**: Predict rankings for new rides
4. **Weighted Ratings**: Consider recency or reviewer credibility
## Technical Details
### File Structure
```
apps/rides/
├── models/
│ └── rankings.py # Ranking data models
├── services/
│ └── ranking_service.py # Algorithm implementation
├── management/
│ └── commands/
│ └── update_ride_rankings.py # CLI command
└── admin.py # Admin interface configuration
```
### Database Tables
- `rides_rideranking`: Current rankings
- `rides_ridepaircomparison`: Cached comparisons
- `rides_rankingsnapshot`: Historical data
- `rides_riderankingevent`: Audit trail (pghistory)
- `rides_ridepaircomparisonevent`: Audit trail (pghistory)
### Dependencies
- Django 5.2+
- PostgreSQL with pghistory
- Python 3.11+
## References
- [Internet Roller Coaster Poll](https://ushsho.com/ridesurvey.py) - Original algorithm source
- [Condorcet Method](https://en.wikipedia.org/wiki/Condorcet_method) - Similar voting system theory
- [Pairwise Comparison](https://en.wikipedia.org/wiki/Pairwise_comparison) - Mathematical foundation

View File

@@ -0,0 +1,164 @@
# ThrillWiki Trending System - Technical Architecture
## System Components Overview
```mermaid
graph TB
subgraph "Frontend Layer"
A[Home.vue Component] --> B[API Service Layer]
B --> C[Trending Content Display]
B --> D[New Content Display]
end
subgraph "API Layer"
E[/api/v1/trending/] --> F[TrendingViewSet]
G[/api/v1/new-content/] --> H[NewContentViewSet]
F --> I[TrendingSerializer]
H --> J[NewContentSerializer]
end
subgraph "Business Logic"
K[Trending Algorithm] --> L[Score Calculator]
L --> M[Weight Processor]
M --> N[Ranking Engine]
end
subgraph "Data Layer"
O[PageView Model] --> P[View Tracker]
Q[Park Model] --> R[Content Source]
S[Ride Model] --> R
T[pghistory Events] --> U[Change Tracker]
end
subgraph "Caching Layer"
V[Redis Cache] --> W[Trending Cache]
V --> X[New Content Cache]
W --> Y[6hr TTL]
X --> Z[24hr TTL]
end
subgraph "Background Tasks"
AA[Management Command] --> BB[Calculate Trending]
CC[Celery/Cron Scheduler] --> AA
BB --> K
BB --> V
end
subgraph "Middleware"
DD[View Tracking Middleware] --> O
EE[User Request] --> DD
end
A --> E
A --> G
F --> K
H --> U
K --> O
K --> Q
K --> S
F --> V
H --> V
EE --> A
```
## Data Flow Architecture
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant API as API Layer
participant C as Cache
participant BG as Background Job
participant DB as Database
participant M as Middleware
Note over U,M: Page View Tracking
U->>F: Visit Park/Ride Page
F->>M: HTTP Request
M->>DB: Store PageView Record
Note over U,M: Trending Content Request
U->>F: Load Home Page
F->>API: GET /api/v1/trending/?tab=rides
API->>C: Check Cache
alt Cache Hit
C->>API: Return Cached Data
else Cache Miss
API->>DB: Query Trending Data
DB->>API: Raw Data
API->>API: Apply Algorithm
API->>C: Store in Cache
end
API->>F: Trending Response
F->>U: Display Trending Content
Note over U,M: Background Processing
BG->>DB: Aggregate PageViews
BG->>DB: Calculate Scores
BG->>C: Update Cache
```
## Algorithm Flow
```mermaid
flowchart TD
A[Start Trending Calculation] --> B[Fetch Recent PageViews]
B --> C[Group by Content Type/ID]
C --> D[Calculate View Score]
D --> E[Fetch Content Ratings]
E --> F[Calculate Rating Score]
F --> G[Calculate Recency Score]
G --> H[Apply Weighted Formula]
H --> I{Score > Threshold?}
I -->|Yes| J[Add to Trending List]
I -->|No| K[Skip Item]
J --> L[Sort by Final Score]
K --> L
L --> M[Cache Results]
M --> N[End]
```
## Database Schema Relationships
```mermaid
erDiagram
PageView ||--o{ ContentType : references
PageView {
id bigint PK
content_type_id int FK
object_id int
user_session varchar
ip_address inet
user_agent text
timestamp datetime
}
Park ||--o{ PageView : tracked_in
Park {
id int PK
name varchar
slug varchar
average_rating decimal
status varchar
opening_date date
closing_date date
}
Ride ||--o{ PageView : tracked_in
Ride {
id int PK
name varchar
slug varchar
park_id int FK
average_rating decimal
category varchar
status varchar
opening_date date
}
TrendingCache {
key varchar PK
data json
expires_at datetime
}

View File

@@ -0,0 +1,140 @@
# ThrillWiki Trending & New Content System Architecture
## System Overview
This document outlines the architecture for implementing real trending and new content functionality to replace the current mock data implementation on the ThrillWiki home page.
## Current State Analysis
### Frontend Structure (Vue 3 + TypeScript)
- **Home.vue** expects specific data formats:
- **Trending Content**: `{id, name, location, category, rating, rank, views, views_change, slug}`
- **New Content**: `{id, name, location, category, date_added, slug}`
- **Tabs Supported**:
- Trending: Rides, Parks, Reviews
- New: Recently Added, Newly Opened, Upcoming
### Backend Infrastructure
- **Django REST Framework** with comprehensive ViewSets
- **pghistory** already tracking model changes
- **Existing endpoints** for recent changes, openings, closures
- **Models**: Park and Ride with ratings, status, dates
## Proposed Architecture
### 1. Data Flow Architecture
```mermaid
flowchart TD
A[User Views Page] --> B[View Tracking Middleware]
B --> C[PageView Model]
D[Trending Calculation Job] --> E[Trending Algorithm]
E --> F[Cache Layer]
G[Frontend Request] --> H[API Endpoints]
H --> F
F --> I[Serialized Response]
I --> J[Frontend Display]
K[Management Command] --> D
L[Celery/Cron Schedule] --> K
```
### 2. Database Schema Design
#### PageView Model
```python
class PageView(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
user_session = models.CharField(max_length=40)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['content_type', 'object_id', 'timestamp']),
models.Index(fields=['timestamp']),
]
```
### 3. Trending Algorithm
#### Calculation Components
- **View Count Weight**: Recent page views (configurable time window)
- **Rating Weight**: Average rating from Park/Ride models
- **Recency Boost**: Recently added/updated content bonus
- **Category Balancing**: Ensure diverse content across categories
#### Formula
```
Trending Score = (View Score × 0.4) + (Rating Score × 0.3) + (Recency Score × 0.2) + (Engagement Score × 0.1)
```
### 4. API Endpoints Design
#### Trending Endpoint
```
GET /api/v1/trending/?tab={rides|parks|reviews}&limit=6
```
#### New Content Endpoint
```
GET /api/v1/new-content/?tab={recently-added|newly-opened|upcoming}&limit=4
```
### 5. Caching Strategy
#### Cache Keys
- `trending_rides_6h`: Trending rides cache (6 hour TTL)
- `trending_parks_6h`: Trending parks cache (6 hour TTL)
- `new_content_24h`: New content cache (24 hour TTL)
#### Cache Invalidation
- Manual refresh via management command
- Automatic refresh on schedule
- Cache warming during low-traffic periods
### 6. Performance Considerations
#### View Tracking Optimization
- Async middleware for non-blocking view tracking
- Batch insert for high-volume periods
- IP-based rate limiting to prevent spam
#### Database Optimization
- Proper indexing on PageView model
- Aggregate tables for trending calculations
- Periodic cleanup of old PageView records
## Implementation Plan
The implementation follows the todo list with these key phases:
1. **Database Layer**: PageView model and migrations
2. **Algorithm Design**: Trending calculation logic
3. **API Layer**: New endpoints and serializers
4. **Tracking System**: Middleware for view capture
5. **Caching Layer**: Performance optimization
6. **Automation**: Management commands and scheduling
7. **Frontend Integration**: Replace mock data
8. **Testing & Monitoring**: Comprehensive coverage
## Security & Privacy
- Anonymous view tracking (no personal data)
- Session-based rate limiting
- User agent validation
- IP address anonymization options
## Monitoring & Analytics
- View tracking success rates
- Trending calculation performance
- Cache hit/miss ratios
- API response times
- Algorithm effectiveness metrics