Refactor API structure and add comprehensive user management features

- Restructure API v1 with improved serializers organization
- Add user deletion requests and moderation queue system
- Implement bulk moderation operations and permissions
- Add user profile enhancements with display names and avatars
- Expand ride and park API endpoints with better filtering
- Add manufacturer API with detailed ride relationships
- Improve authentication flows and error handling
- Update frontend documentation and API specifications
This commit is contained in:
pacnpal
2025-08-29 16:03:51 -04:00
parent 7b9f64be72
commit bb7da85516
92 changed files with 19690 additions and 9076 deletions

View File

@@ -9,14 +9,20 @@ 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.db.models import Count
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 datetime import datetime
from apps.parks.models import Park, ParkReview, ParkPhoto, Company as ParkCompany
from apps.rides.models import Ride, RollerCoasterStats, RideReview, RidePhoto, Company as RideCompany
from apps.rides.models import (
Ride,
RollerCoasterStats,
RideReview,
RidePhoto,
Company as RideCompany,
)
from ..serializers.stats import StatsSerializer
@@ -40,13 +46,13 @@ class StatsAPIView(APIView):
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'
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'))
timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
else:
timestamp = timestamp_str
@@ -60,7 +66,7 @@ class StatsAPIView(APIView):
# If less than a minute, return "just now"
if total_seconds < 60:
return 'just now'
return "just now"
# Calculate time components
days = diff.days
@@ -81,16 +87,16 @@ class StatsAPIView(APIView):
# Join parts with commas and add "ago"
if len(parts) == 0:
return 'just now'
return "just now"
elif len(parts) == 1:
return f'{parts[0]} ago'
return f"{parts[0]} ago"
elif len(parts) == 2:
return f'{parts[0]} and {parts[1]} ago'
return f"{parts[0]} and {parts[1]} ago"
else:
return f'{", ".join(parts[:-1])}, and {parts[-1]} ago'
except (ValueError, TypeError):
return 'unknown'
return "unknown"
@extend_schema(
operation_id="get_platform_stats",
@@ -115,9 +121,12 @@ class StatsAPIView(APIView):
500: {
"type": "object",
"properties": {
"error": {"type": "string", "description": "Error message if statistics calculation fails"}
}
}
"error": {
"type": "string",
"description": "Error message if statistics calculation fails",
}
},
},
},
tags=["Statistics"],
examples=[
@@ -142,10 +151,10 @@ class StatsAPIView(APIView):
"operating_parks": 7,
"operating_rides": 10,
"last_updated": "2025-08-28T17:34:59.677143+00:00",
"relative_last_updated": "just now"
}
"relative_last_updated": "just now",
},
)
]
],
)
def get(self, request):
"""Get platform statistics."""
@@ -197,76 +206,76 @@ class StatsAPIView(APIView):
total_roller_coasters = RollerCoasterStats.objects.count()
# Ride category counts
ride_categories = Ride.objects.values('category').annotate(
count=Count('id')
).exclude(category='')
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']
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'
"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_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_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']
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'
"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()}')
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_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']
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'
"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()}')
status_code, f"ride_status_{status_code.lower()}"
)
ride_status_stats[status_name] = status_count
# Review counts
@@ -279,13 +288,13 @@ class StatsAPIView(APIView):
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':
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)
cache.set("platform_stats_timestamp", last_updated_iso, 300)
# Calculate relative time
relative_last_updated = self._get_relative_time(last_updated_iso)
@@ -293,34 +302,29 @@ class StatsAPIView(APIView):
# 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,
"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,
"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,
"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
"last_updated": last_updated_iso,
"relative_last_updated": relative_last_updated,
}
return stats
@@ -351,8 +355,11 @@ class StatsRecalculateAPIView(APIView):
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)
return Response(
{
"message": "Platform statistics have been successfully recalculated",
"stats": fresh_stats,
"recalculated_at": timezone.now().isoformat(),
},
status=status.HTTP_200_OK,
)