mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user