mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 04:31:09 -05:00
359 lines
12 KiB
Python
359 lines
12 KiB
Python
"""
|
|
Statistics API views for ThrillWiki.
|
|
|
|
Provides aggregate statistics about the platform's content including
|
|
counts of parks, rides, manufacturers, and other entities.
|
|
"""
|
|
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from rest_framework.permissions import AllowAny, IsAdminUser
|
|
from django.db.models import Count, Q
|
|
from django.core.cache import cache
|
|
from django.utils import timezone
|
|
from drf_spectacular.utils import extend_schema, OpenApiExample
|
|
from datetime import datetime, timedelta
|
|
|
|
from apps.parks.models import Park, ParkReview, ParkPhoto, Company as ParkCompany
|
|
from apps.rides.models import Ride, RollerCoasterStats, RideReview, RidePhoto, Company as RideCompany
|
|
from ..serializers.stats import StatsSerializer
|
|
|
|
|
|
class StatsAPIView(APIView):
|
|
"""
|
|
API endpoint that returns aggregate statistics about the platform.
|
|
|
|
Returns counts of various entities like parks, rides, manufacturers, etc.
|
|
Results are cached for performance.
|
|
"""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def _get_relative_time(self, timestamp_str):
|
|
"""
|
|
Convert an ISO timestamp to a human-readable relative time.
|
|
|
|
Args:
|
|
timestamp_str: ISO format timestamp string
|
|
|
|
Returns:
|
|
str: Human-readable relative time (e.g., "2 days, 3 hours, 15 minutes ago", "just now")
|
|
"""
|
|
if not timestamp_str or timestamp_str == 'just_now':
|
|
return 'just now'
|
|
|
|
try:
|
|
# Parse the ISO timestamp
|
|
if isinstance(timestamp_str, str):
|
|
timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
else:
|
|
timestamp = timestamp_str
|
|
|
|
# Make timezone-aware if needed
|
|
if timestamp.tzinfo is None:
|
|
timestamp = timezone.make_aware(timestamp)
|
|
|
|
now = timezone.now()
|
|
diff = now - timestamp
|
|
total_seconds = int(diff.total_seconds())
|
|
|
|
# If less than a minute, return "just now"
|
|
if total_seconds < 60:
|
|
return 'just now'
|
|
|
|
# Calculate time components
|
|
days = diff.days
|
|
hours = (total_seconds % 86400) // 3600
|
|
minutes = (total_seconds % 3600) // 60
|
|
|
|
# Build the relative time string
|
|
parts = []
|
|
|
|
if days > 0:
|
|
parts.append(f'{days} day{"s" if days != 1 else ""}')
|
|
|
|
if hours > 0:
|
|
parts.append(f'{hours} hour{"s" if hours != 1 else ""}')
|
|
|
|
if minutes > 0:
|
|
parts.append(f'{minutes} minute{"s" if minutes != 1 else ""}')
|
|
|
|
# Join parts with commas and add "ago"
|
|
if len(parts) == 0:
|
|
return 'just now'
|
|
elif len(parts) == 1:
|
|
return f'{parts[0]} ago'
|
|
elif len(parts) == 2:
|
|
return f'{parts[0]} and {parts[1]} ago'
|
|
else:
|
|
return f'{", ".join(parts[:-1])}, and {parts[-1]} ago'
|
|
|
|
except (ValueError, TypeError):
|
|
return 'unknown'
|
|
|
|
@extend_schema(
|
|
operation_id="get_platform_stats",
|
|
summary="Get platform statistics",
|
|
description="""
|
|
Returns comprehensive aggregate statistics about the ThrillWiki platform.
|
|
|
|
This endpoint provides detailed counts and breakdowns of all major entities including:
|
|
- Parks, rides, and roller coasters
|
|
- Companies (manufacturers, operators, designers, property owners)
|
|
- Photos and reviews
|
|
- Ride categories (roller coasters, dark rides, flat rides, etc.)
|
|
- Status breakdowns (operating, closed, under construction, etc.)
|
|
|
|
Results are cached for 5 minutes for optimal performance and automatically
|
|
invalidated when relevant data changes.
|
|
|
|
**No authentication required** - this is a public endpoint.
|
|
""".strip(),
|
|
responses={
|
|
200: StatsSerializer,
|
|
500: {
|
|
"type": "object",
|
|
"properties": {
|
|
"error": {"type": "string", "description": "Error message if statistics calculation fails"}
|
|
}
|
|
}
|
|
},
|
|
tags=["Statistics"],
|
|
examples=[
|
|
OpenApiExample(
|
|
name="Sample Response",
|
|
description="Example of platform statistics response",
|
|
value={
|
|
"total_parks": 7,
|
|
"total_rides": 10,
|
|
"total_manufacturers": 6,
|
|
"total_operators": 7,
|
|
"total_designers": 4,
|
|
"total_property_owners": 0,
|
|
"total_roller_coasters": 8,
|
|
"total_photos": 0,
|
|
"total_park_photos": 0,
|
|
"total_ride_photos": 0,
|
|
"total_reviews": 8,
|
|
"total_park_reviews": 4,
|
|
"total_ride_reviews": 4,
|
|
"roller_coasters": 10,
|
|
"operating_parks": 7,
|
|
"operating_rides": 10,
|
|
"last_updated": "2025-08-28T17:34:59.677143+00:00",
|
|
"relative_last_updated": "just now"
|
|
}
|
|
)
|
|
]
|
|
)
|
|
def get(self, request):
|
|
"""Get platform statistics."""
|
|
# Try to get cached stats first
|
|
cache_key = "platform_stats"
|
|
cached_stats = cache.get(cache_key)
|
|
|
|
if cached_stats:
|
|
return Response(cached_stats, status=status.HTTP_200_OK)
|
|
|
|
# Calculate fresh stats
|
|
stats = self._calculate_stats()
|
|
|
|
# Cache for 5 minutes
|
|
cache.set(cache_key, stats, 300)
|
|
|
|
return Response(stats, status=status.HTTP_200_OK)
|
|
|
|
def _calculate_stats(self):
|
|
"""Calculate all platform statistics."""
|
|
|
|
# Basic entity counts
|
|
total_parks = Park.objects.count()
|
|
total_rides = Ride.objects.count()
|
|
|
|
# Company counts by role
|
|
total_manufacturers = RideCompany.objects.filter(
|
|
roles__contains=["MANUFACTURER"]
|
|
).count()
|
|
|
|
total_operators = ParkCompany.objects.filter(
|
|
roles__contains=["OPERATOR"]
|
|
).count()
|
|
|
|
total_designers = RideCompany.objects.filter(
|
|
roles__contains=["DESIGNER"]
|
|
).count()
|
|
|
|
total_property_owners = ParkCompany.objects.filter(
|
|
roles__contains=["PROPERTY_OWNER"]
|
|
).count()
|
|
|
|
# Photo counts (combined)
|
|
total_park_photos = ParkPhoto.objects.count()
|
|
total_ride_photos = RidePhoto.objects.count()
|
|
total_photos = total_park_photos + total_ride_photos
|
|
|
|
# Ride type counts
|
|
total_roller_coasters = RollerCoasterStats.objects.count()
|
|
|
|
# Ride category counts
|
|
ride_categories = Ride.objects.values('category').annotate(
|
|
count=Count('id')
|
|
).exclude(category='')
|
|
|
|
category_stats = {}
|
|
for category in ride_categories:
|
|
category_code = category['category']
|
|
category_count = category['count']
|
|
|
|
# Convert category codes to readable names
|
|
category_names = {
|
|
'RC': 'roller_coasters',
|
|
'DR': 'dark_rides',
|
|
'FR': 'flat_rides',
|
|
'WR': 'water_rides',
|
|
'TR': 'transport_rides',
|
|
'OT': 'other_rides'
|
|
}
|
|
|
|
category_name = category_names.get(
|
|
category_code, f'category_{category_code.lower()}')
|
|
category_stats[category_name] = category_count
|
|
|
|
# Park status counts
|
|
park_statuses = Park.objects.values('status').annotate(
|
|
count=Count('id')
|
|
)
|
|
|
|
park_status_stats = {}
|
|
for status_item in park_statuses:
|
|
status_code = status_item['status']
|
|
status_count = status_item['count']
|
|
|
|
# Convert status codes to readable names
|
|
status_names = {
|
|
'OPERATING': 'operating_parks',
|
|
'CLOSED_TEMP': 'temporarily_closed_parks',
|
|
'CLOSED_PERM': 'permanently_closed_parks',
|
|
'UNDER_CONSTRUCTION': 'under_construction_parks',
|
|
'DEMOLISHED': 'demolished_parks',
|
|
'RELOCATED': 'relocated_parks'
|
|
}
|
|
|
|
status_name = status_names.get(status_code, f'status_{status_code.lower()}')
|
|
park_status_stats[status_name] = status_count
|
|
|
|
# Ride status counts
|
|
ride_statuses = Ride.objects.values('status').annotate(
|
|
count=Count('id')
|
|
)
|
|
|
|
ride_status_stats = {}
|
|
for status_item in ride_statuses:
|
|
status_code = status_item['status']
|
|
status_count = status_item['count']
|
|
|
|
# Convert status codes to readable names
|
|
status_names = {
|
|
'OPERATING': 'operating_rides',
|
|
'CLOSED_TEMP': 'temporarily_closed_rides',
|
|
'SBNO': 'sbno_rides',
|
|
'CLOSING': 'closing_rides',
|
|
'CLOSED_PERM': 'permanently_closed_rides',
|
|
'UNDER_CONSTRUCTION': 'under_construction_rides',
|
|
'DEMOLISHED': 'demolished_rides',
|
|
'RELOCATED': 'relocated_rides'
|
|
}
|
|
|
|
status_name = status_names.get(
|
|
status_code, f'ride_status_{status_code.lower()}')
|
|
ride_status_stats[status_name] = status_count
|
|
|
|
# Review counts
|
|
total_park_reviews = ParkReview.objects.count()
|
|
total_ride_reviews = RideReview.objects.count()
|
|
total_reviews = total_park_reviews + total_ride_reviews
|
|
|
|
# Timestamp handling
|
|
now = timezone.now()
|
|
last_updated_iso = now.isoformat()
|
|
|
|
# Get cached timestamp or use current time
|
|
cached_timestamp = cache.get('platform_stats_timestamp')
|
|
if cached_timestamp and cached_timestamp != 'just_now':
|
|
# Use cached timestamp for consistency
|
|
last_updated_iso = cached_timestamp
|
|
else:
|
|
# Set new timestamp in cache
|
|
cache.set('platform_stats_timestamp', last_updated_iso, 300)
|
|
|
|
# Calculate relative time
|
|
relative_last_updated = self._get_relative_time(last_updated_iso)
|
|
|
|
# Combine all stats
|
|
stats = {
|
|
# Core entity counts
|
|
'total_parks': total_parks,
|
|
'total_rides': total_rides,
|
|
'total_manufacturers': total_manufacturers,
|
|
'total_operators': total_operators,
|
|
'total_designers': total_designers,
|
|
'total_property_owners': total_property_owners,
|
|
'total_roller_coasters': total_roller_coasters,
|
|
|
|
# Photo counts
|
|
'total_photos': total_photos,
|
|
'total_park_photos': total_park_photos,
|
|
'total_ride_photos': total_ride_photos,
|
|
|
|
# Review counts
|
|
'total_reviews': total_reviews,
|
|
'total_park_reviews': total_park_reviews,
|
|
'total_ride_reviews': total_ride_reviews,
|
|
|
|
# Category breakdowns
|
|
**category_stats,
|
|
|
|
# Status breakdowns
|
|
**park_status_stats,
|
|
**ride_status_stats,
|
|
|
|
# Metadata
|
|
'last_updated': last_updated_iso,
|
|
'relative_last_updated': relative_last_updated
|
|
}
|
|
|
|
return stats
|
|
|
|
|
|
class StatsRecalculateAPIView(APIView):
|
|
"""
|
|
Admin-only API endpoint to force recalculation of platform statistics.
|
|
|
|
This endpoint clears the cache and forces a fresh calculation of all statistics.
|
|
Only accessible to admin users.
|
|
"""
|
|
|
|
permission_classes = [IsAdminUser]
|
|
|
|
@extend_schema(exclude=True)
|
|
def post(self, request):
|
|
"""Force recalculation of platform statistics."""
|
|
# Clear the cache
|
|
cache.delete("platform_stats")
|
|
cache.delete("platform_stats_timestamp")
|
|
|
|
# Create a new StatsAPIView instance to reuse the calculation logic
|
|
stats_view = StatsAPIView()
|
|
fresh_stats = stats_view._calculate_stats()
|
|
|
|
# Cache the fresh stats
|
|
cache.set("platform_stats", fresh_stats, 300)
|
|
|
|
# Return success response with the fresh stats
|
|
return Response({
|
|
"message": "Platform statistics have been successfully recalculated",
|
|
"stats": fresh_stats,
|
|
"recalculated_at": timezone.now().isoformat()
|
|
}, status=status.HTTP_200_OK)
|