Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-08-26 13:19:04 -04:00
parent bf7e0c0f40
commit 831be6a2ee
151 changed files with 16260 additions and 9137 deletions

View File

@@ -0,0 +1,51 @@
"""
API v1 Views Package
This package contains all API view classes organized by functionality:
- auth.py: Authentication and user management views
- health.py: Health check and monitoring views
- trending.py: Trending and new content discovery views
"""
# Import all view classes for easy access
from .auth import (
LoginAPIView,
SignupAPIView,
LogoutAPIView,
CurrentUserAPIView,
PasswordResetAPIView,
PasswordChangeAPIView,
SocialProvidersAPIView,
AuthStatusAPIView,
)
from .health import (
HealthCheckAPIView,
PerformanceMetricsAPIView,
SimpleHealthAPIView,
)
from .trending import (
TrendingAPIView,
NewContentAPIView,
)
# Export all views for import convenience
__all__ = [
# Authentication views
"LoginAPIView",
"SignupAPIView",
"LogoutAPIView",
"CurrentUserAPIView",
"PasswordResetAPIView",
"PasswordChangeAPIView",
"SocialProvidersAPIView",
"AuthStatusAPIView",
# Health check views
"HealthCheckAPIView",
"PerformanceMetricsAPIView",
"SimpleHealthAPIView",
# Trending views
"TrendingAPIView",
"NewContentAPIView",
]

View File

@@ -0,0 +1,468 @@
"""
Authentication API views for ThrillWiki API v1.
This module contains all authentication-related API endpoints including
login, signup, logout, password management, and social authentication.
"""
import time
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view
# Import serializers inside methods to avoid Django initialization issues
# Placeholder classes for schema decorators
class LoginInputSerializer:
pass
class LoginOutputSerializer:
pass
class SignupInputSerializer:
pass
class SignupOutputSerializer:
pass
class LogoutOutputSerializer:
pass
class UserOutputSerializer:
pass
class PasswordResetInputSerializer:
pass
class PasswordResetOutputSerializer:
pass
class PasswordChangeInputSerializer:
pass
class PasswordChangeOutputSerializer:
pass
class SocialProviderOutputSerializer:
pass
class AuthStatusOutputSerializer:
pass
# Handle optional dependencies with fallback classes
class FallbackTurnstileMixin:
"""Fallback mixin if TurnstileMixin is not available."""
def validate_turnstile(self, request):
pass
# Try to import the real class, use fallback if not available
try:
from apps.accounts.mixins import TurnstileMixin
except ImportError:
TurnstileMixin = FallbackTurnstileMixin
UserModel = get_user_model()
@extend_schema_view(
post=extend_schema(
summary="User login",
description="Authenticate user with username/email and password.",
request=LoginInputSerializer,
responses={
200: LoginOutputSerializer,
400: "Bad Request",
},
tags=["Authentication"],
),
)
class LoginAPIView(TurnstileMixin, APIView):
"""API endpoint for user login."""
permission_classes = [AllowAny]
authentication_classes = []
serializer_class = LoginInputSerializer
def post(self, request: Request) -> Response:
from ..serializers import LoginInputSerializer, LoginOutputSerializer
try:
# Validate Turnstile if configured
self.validate_turnstile(request)
except ValidationError as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
serializer = LoginInputSerializer(data=request.data)
if serializer.is_valid():
# type: ignore[index]
email_or_username = serializer.validated_data["username"]
password = serializer.validated_data["password"] # type: ignore[index]
# Optimized user lookup: single query using Q objects
from django.db.models import Q
from django.contrib.auth import get_user_model
User = get_user_model()
user = None
# Single query to find user by email OR username
try:
if "@" in email_or_username:
# Email-like input: try email first, then username as fallback
user_obj = (
User.objects.select_related()
.filter(
Q(email=email_or_username) | Q(username=email_or_username)
)
.first()
)
else:
# Username-like input: try username first, then email as fallback
user_obj = (
User.objects.select_related()
.filter(
Q(username=email_or_username) | Q(email=email_or_username)
)
.first()
)
if user_obj:
user = authenticate(
# type: ignore[attr-defined]
request._request,
username=user_obj.username,
password=password,
)
except Exception:
# Fallback to original behavior
user = authenticate(
# type: ignore[attr-defined]
request._request,
username=email_or_username,
password=password,
)
if user:
if user.is_active:
login(request._request, user) # type: ignore[attr-defined]
# Optimized token creation - get_or_create is atomic
from rest_framework.authtoken.models import Token
token, created = Token.objects.get_or_create(user=user)
response_serializer = LoginOutputSerializer(
{
"token": token.key,
"user": user,
"message": "Login successful",
}
)
return Response(response_serializer.data)
else:
return Response(
{"error": "Account is disabled"},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"error": "Invalid credentials"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_view(
post=extend_schema(
summary="User registration",
description="Register a new user account.",
request=SignupInputSerializer,
responses={
201: SignupOutputSerializer,
400: "Bad Request",
},
tags=["Authentication"],
),
)
class SignupAPIView(TurnstileMixin, APIView):
"""API endpoint for user registration."""
permission_classes = [AllowAny]
authentication_classes = []
serializer_class = SignupInputSerializer
def post(self, request: Request) -> Response:
try:
# Validate Turnstile if configured
self.validate_turnstile(request)
except ValidationError as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
serializer = SignupInputSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
login(request._request, user) # type: ignore[attr-defined]
from rest_framework.authtoken.models import Token
token, created = Token.objects.get_or_create(user=user)
response_serializer = SignupOutputSerializer(
{
"token": token.key,
"user": user,
"message": "Registration successful",
}
)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_view(
post=extend_schema(
summary="User logout",
description="Logout the current user and invalidate their token.",
responses={
200: LogoutOutputSerializer,
401: "Unauthorized",
},
tags=["Authentication"],
),
)
class LogoutAPIView(APIView):
"""API endpoint for user logout."""
permission_classes = [IsAuthenticated]
serializer_class = LogoutOutputSerializer
def post(self, request: Request) -> Response:
try:
# Delete the token for token-based auth
if hasattr(request.user, "auth_token"):
request.user.auth_token.delete()
# Logout from session
logout(request._request) # type: ignore[attr-defined]
response_serializer = LogoutOutputSerializer(
{"message": "Logout successful"}
)
return Response(response_serializer.data)
except Exception as e:
return Response(
{"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema_view(
get=extend_schema(
summary="Get current user",
description="Retrieve information about the currently authenticated user.",
responses={
200: UserOutputSerializer,
401: "Unauthorized",
},
tags=["Authentication"],
),
)
class CurrentUserAPIView(APIView):
"""API endpoint to get current user information."""
permission_classes = [IsAuthenticated]
serializer_class = UserOutputSerializer
def get(self, request: Request) -> Response:
serializer = UserOutputSerializer(request.user)
return Response(serializer.data)
@extend_schema_view(
post=extend_schema(
summary="Request password reset",
description="Send a password reset email to the user.",
request=PasswordResetInputSerializer,
responses={
200: PasswordResetOutputSerializer,
400: "Bad Request",
},
tags=["Authentication"],
),
)
class PasswordResetAPIView(APIView):
"""API endpoint to request password reset."""
permission_classes = [AllowAny]
serializer_class = PasswordResetInputSerializer
def post(self, request: Request) -> Response:
serializer = PasswordResetInputSerializer(
data=request.data, context={"request": request}
)
if serializer.is_valid():
serializer.save()
response_serializer = PasswordResetOutputSerializer(
{"detail": "Password reset email sent"}
)
return Response(response_serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_view(
post=extend_schema(
summary="Change password",
description="Change the current user's password.",
request=PasswordChangeInputSerializer,
responses={
200: PasswordChangeOutputSerializer,
400: "Bad Request",
401: "Unauthorized",
},
tags=["Authentication"],
),
)
class PasswordChangeAPIView(APIView):
"""API endpoint to change password."""
permission_classes = [IsAuthenticated]
serializer_class = PasswordChangeInputSerializer
def post(self, request: Request) -> Response:
serializer = PasswordChangeInputSerializer(
data=request.data, context={"request": request}
)
if serializer.is_valid():
serializer.save()
response_serializer = PasswordChangeOutputSerializer(
{"detail": "Password changed successfully"}
)
return Response(response_serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_view(
get=extend_schema(
summary="Get social providers",
description="Retrieve available social authentication providers.",
responses={200: "List of social providers"},
tags=["Authentication"],
),
)
class SocialProvidersAPIView(APIView):
"""API endpoint to get available social authentication providers."""
permission_classes = [AllowAny]
serializer_class = SocialProviderOutputSerializer
def get(self, request: Request) -> Response:
from django.core.cache import cache
from django.contrib.sites.shortcuts import get_current_site
site = get_current_site(request._request) # type: ignore[attr-defined]
# Cache key based on site and request host
cache_key = (
f"social_providers:{getattr(site, 'id', site.pk)}:{request.get_host()}"
)
# Try to get from cache first (cache for 15 minutes)
cached_providers = cache.get(cache_key)
if cached_providers is not None:
return Response(cached_providers)
providers_list = []
# Optimized query: filter by site and order by provider name
from allauth.socialaccount.models import SocialApp
social_apps = SocialApp.objects.filter(sites=site).order_by("provider")
for social_app in social_apps:
try:
# Simplified provider name resolution - avoid expensive provider class loading
provider_name = social_app.name or social_app.provider.title()
# Build auth URL efficiently
auth_url = request.build_absolute_uri(
f"/accounts/{social_app.provider}/login/"
)
providers_list.append(
{
"id": social_app.provider,
"name": provider_name,
"authUrl": auth_url,
}
)
except Exception:
# Skip if provider can't be loaded
continue
# Serialize and cache the result
serializer = SocialProviderOutputSerializer(providers_list, many=True)
response_data = serializer.data
# Cache for 15 minutes (900 seconds)
cache.set(cache_key, response_data, 900)
return Response(response_data)
@extend_schema_view(
post=extend_schema(
summary="Check authentication status",
description="Check if user is authenticated and return user data.",
responses={200: AuthStatusOutputSerializer},
tags=["Authentication"],
),
)
class AuthStatusAPIView(APIView):
"""API endpoint to check authentication status."""
permission_classes = [AllowAny]
serializer_class = AuthStatusOutputSerializer
def post(self, request: Request) -> Response:
if request.user.is_authenticated:
response_data = {
"authenticated": True,
"user": request.user,
}
else:
response_data = {
"authenticated": False,
"user": None,
}
serializer = AuthStatusOutputSerializer(response_data)
return Response(serializer.data)

View File

@@ -0,0 +1,351 @@
"""
Health check API views for ThrillWiki API v1.
This module contains health check and monitoring endpoints for system status,
performance metrics, and database analysis.
"""
import time
from django.utils import timezone
from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from health_check.views import MainView
from drf_spectacular.utils import extend_schema, extend_schema_view
# Import serializers
from ..serializers import (
HealthCheckOutputSerializer,
PerformanceMetricsOutputSerializer,
SimpleHealthOutputSerializer,
)
# Handle optional dependencies with fallback classes
class FallbackCacheMonitor:
"""Fallback class if CacheMonitor is not available."""
def get_cache_stats(self):
return {"error": "Cache monitoring not available"}
class FallbackIndexAnalyzer:
"""Fallback class if IndexAnalyzer is not available."""
@staticmethod
def analyze_slow_queries(threshold):
return {"error": "Query analysis not available"}
# Try to import the real classes, use fallbacks if not available
try:
from apps.core.services.enhanced_cache_service import CacheMonitor
except ImportError:
CacheMonitor = FallbackCacheMonitor
try:
from apps.core.utils.query_optimization import IndexAnalyzer
except ImportError:
IndexAnalyzer = FallbackIndexAnalyzer
@extend_schema_view(
get=extend_schema(
summary="Health check",
description="Get comprehensive health check information including system metrics.",
responses={
200: HealthCheckOutputSerializer,
503: HealthCheckOutputSerializer,
},
tags=["Health"],
),
)
class HealthCheckAPIView(APIView):
"""Enhanced API endpoint for health checks with detailed JSON response."""
permission_classes = [AllowAny]
serializer_class = HealthCheckOutputSerializer
def get(self, request: Request) -> Response:
"""Return comprehensive health check information."""
start_time = time.time()
# Get basic health check results
main_view = MainView()
main_view.request = request._request # type: ignore[attr-defined]
plugins = main_view.plugins
errors = main_view.errors
# Collect additional performance metrics
try:
cache_monitor = CacheMonitor()
cache_stats = cache_monitor.get_cache_stats()
except Exception:
cache_stats = {"error": "Cache monitoring unavailable"}
# Build comprehensive health data
health_data = {
"status": "healthy" if not errors else "unhealthy",
"timestamp": timezone.now(),
"version": getattr(settings, "VERSION", "1.0.0"),
"environment": getattr(settings, "ENVIRONMENT", "development"),
"response_time_ms": 0, # Will be calculated at the end
"checks": {},
"metrics": {
"cache": cache_stats,
"database": self._get_database_metrics(),
"system": self._get_system_metrics(),
},
}
# Process individual health checks
for plugin in plugins:
plugin_name = plugin.identifier()
plugin_errors = (
errors.get(plugin.__class__.__name__, [])
if isinstance(errors, dict)
else []
)
health_data["checks"][plugin_name] = {
"status": "healthy" if not plugin_errors else "unhealthy",
"critical": getattr(plugin, "critical_service", False),
"errors": [str(error) for error in plugin_errors],
"response_time_ms": getattr(plugin, "_response_time", None),
}
# Calculate total response time
health_data["response_time_ms"] = round((time.time() - start_time) * 1000, 2)
# Determine HTTP status code
status_code = 200
if errors:
# Check if any critical services are failing
critical_errors = any(
getattr(plugin, "critical_service", False)
for plugin in plugins
if isinstance(errors, dict) and errors.get(plugin.__class__.__name__)
)
status_code = 503 if critical_errors else 200
serializer = HealthCheckOutputSerializer(health_data)
return Response(serializer.data, status=status_code)
def _get_database_metrics(self):
"""Get database performance metrics."""
try:
from django.db import connection
# Get basic connection info
metrics = {
"vendor": connection.vendor,
"connection_status": "connected",
}
# Test query performance
start_time = time.time()
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
query_time = (time.time() - start_time) * 1000
metrics["test_query_time_ms"] = round(query_time, 2)
# PostgreSQL specific metrics
if connection.vendor == "postgresql":
try:
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT
numbackends as active_connections,
xact_commit as transactions_committed,
xact_rollback as transactions_rolled_back,
blks_read as blocks_read,
blks_hit as blocks_hit
FROM pg_stat_database
WHERE datname = current_database()
"""
)
row = cursor.fetchone()
if row:
metrics.update(
{ # type: ignore[arg-type]
"active_connections": row[0],
"transactions_committed": row[1],
"transactions_rolled_back": row[2],
"cache_hit_ratio": (
round((row[4] / (row[3] + row[4])) * 100, 2)
if (row[3] + row[4]) > 0
else 0
),
}
)
except Exception:
pass # Skip advanced metrics if not available
return metrics
except Exception as e:
return {"connection_status": "error", "error": str(e)}
def _get_system_metrics(self):
"""Get system performance metrics."""
metrics = {
"debug_mode": settings.DEBUG,
"allowed_hosts": (settings.ALLOWED_HOSTS if settings.DEBUG else ["hidden"]),
}
try:
import psutil
# Memory metrics
memory = psutil.virtual_memory()
metrics["memory"] = {
"total_mb": round(memory.total / 1024 / 1024, 2),
"available_mb": round(memory.available / 1024 / 1024, 2),
"percent_used": memory.percent,
}
# CPU metrics
metrics["cpu"] = {
"percent_used": psutil.cpu_percent(interval=0.1),
"core_count": psutil.cpu_count(),
}
# Disk metrics
disk = psutil.disk_usage("/")
metrics["disk"] = {
"total_gb": round(disk.total / 1024 / 1024 / 1024, 2),
"free_gb": round(disk.free / 1024 / 1024 / 1024, 2),
"percent_used": round((disk.used / disk.total) * 100, 2),
}
except ImportError:
metrics["system_monitoring"] = "psutil not available"
except Exception as e:
metrics["system_error"] = str(e)
return metrics
@extend_schema_view(
get=extend_schema(
summary="Performance metrics",
description="Get performance metrics and database analysis (debug mode only).",
responses={
200: PerformanceMetricsOutputSerializer,
403: "Forbidden",
},
tags=["Health"],
),
)
class PerformanceMetricsAPIView(APIView):
"""API view for performance metrics and database analysis."""
permission_classes = [AllowAny] if settings.DEBUG else []
serializer_class = PerformanceMetricsOutputSerializer
def get(self, request: Request) -> Response:
"""Return performance metrics and analysis."""
if not settings.DEBUG:
return Response({"error": "Only available in debug mode"}, status=403)
metrics = {
"timestamp": timezone.now(),
"database_analysis": self._get_database_analysis(),
"cache_performance": self._get_cache_performance(),
"recent_slow_queries": self._get_slow_queries(),
}
serializer = PerformanceMetricsOutputSerializer(metrics)
return Response(serializer.data)
def _get_database_analysis(self):
"""Analyze database performance."""
try:
from django.db import connection
analysis = {
"total_queries": len(connection.queries),
"query_analysis": IndexAnalyzer.analyze_slow_queries(0.05),
}
if connection.queries:
query_times = [float(q.get("time", 0)) for q in connection.queries]
analysis.update(
{
"total_query_time": sum(query_times),
"average_query_time": sum(query_times) / len(query_times),
"slowest_query_time": max(query_times),
"fastest_query_time": min(query_times),
}
)
return analysis
except Exception as e:
return {"error": str(e)}
def _get_cache_performance(self):
"""Get cache performance metrics."""
try:
cache_monitor = CacheMonitor()
return cache_monitor.get_cache_stats()
except Exception as e:
return {"error": str(e)}
def _get_slow_queries(self):
"""Get recent slow queries."""
try:
return IndexAnalyzer.analyze_slow_queries(0.1) # 100ms threshold
except Exception as e:
return {"error": str(e)}
@extend_schema_view(
get=extend_schema(
summary="Simple health check",
description="Simple health check endpoint for load balancers.",
responses={
200: SimpleHealthOutputSerializer,
503: SimpleHealthOutputSerializer,
},
tags=["Health"],
),
)
class SimpleHealthAPIView(APIView):
"""Simple health check endpoint for load balancers."""
permission_classes = [AllowAny]
serializer_class = SimpleHealthOutputSerializer
def get(self, request: Request) -> Response:
"""Return simple OK status."""
try:
# Basic database connectivity test
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
response_data = {
"status": "ok",
"timestamp": timezone.now(),
}
serializer = SimpleHealthOutputSerializer(response_data)
return Response(serializer.data)
except Exception as e:
response_data = {
"status": "error",
"error": str(e),
"timestamp": timezone.now(),
}
serializer = SimpleHealthOutputSerializer(response_data)
return Response(serializer.data, status=503)

View File

@@ -0,0 +1,364 @@
"""
Trending content API views for ThrillWiki API v1.
This module contains endpoints for trending and new content discovery
including trending parks, rides, and recently added content.
"""
from datetime import datetime, date
from django.utils import timezone
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from drf_spectacular.utils import extend_schema, extend_schema_view
from drf_spectacular.types import OpenApiTypes
@extend_schema_view(
get=extend_schema(
summary="Get trending content",
description="Retrieve trending parks and rides based on view counts, ratings, and recency.",
parameters=[
{
"name": "limit",
"in": "query",
"description": "Number of trending items to return (default: 20, max: 100)",
"required": False,
"schema": {"type": "integer", "default": 20, "maximum": 100},
},
{
"name": "timeframe",
"in": "query",
"description": "Timeframe for trending calculation (day, week, month) - default: week",
"required": False,
"schema": {
"type": "string",
"enum": ["day", "week", "month"],
"default": "week",
},
},
],
responses={200: OpenApiTypes.OBJECT},
tags=["Trending"],
),
)
class TrendingAPIView(APIView):
"""API endpoint for trending content."""
permission_classes = [AllowAny]
def get(self, request: Request) -> Response:
"""Get trending parks and rides."""
try:
from apps.core.services.trending_service import TrendingService
except ImportError:
# Fallback if trending service is not available
return self._get_fallback_trending_content(request)
# Parse parameters
limit = min(int(request.query_params.get("limit", 20)), 100)
# Get trending content
trending_service = TrendingService()
all_trending = trending_service.get_trending_content(limit=limit * 2)
# Separate by content type
trending_rides = []
trending_parks = []
for item in all_trending:
if item.get("category") == "ride":
trending_rides.append(item)
elif item.get("category") == "park":
trending_parks.append(item)
# Limit each category
trending_rides = trending_rides[: limit // 3] if trending_rides else []
trending_parks = trending_parks[: limit // 3] if trending_parks else []
# Create mock latest reviews (since not implemented yet)
latest_reviews = [
{
"id": 1,
"name": "Steel Vengeance Review",
"location": "Cedar Point",
"category": "Roller Coaster",
"rating": 5.0,
"rank": 1,
"views": 1234,
"views_change": "+45%",
"slug": "steel-vengeance-review",
}
][: limit // 3]
# Return in expected frontend format
response_data = {
"trending_rides": trending_rides,
"trending_parks": trending_parks,
"latest_reviews": latest_reviews,
}
return Response(response_data)
def _get_fallback_trending_content(self, request: Request) -> Response:
"""Fallback method when trending service is not available."""
limit = min(int(request.query_params.get("limit", 20)), 100)
# Mock trending data
trending_rides = [
{
"id": 1,
"name": "Steel Vengeance",
"location": "Cedar Point",
"category": "Roller Coaster",
"rating": 4.8,
"rank": 1,
"views": 15234,
"views_change": "+25%",
"slug": "steel-vengeance",
},
{
"id": 2,
"name": "Lightning Rod",
"location": "Dollywood",
"category": "Roller Coaster",
"rating": 4.7,
"rank": 2,
"views": 12456,
"views_change": "+18%",
"slug": "lightning-rod",
},
][: limit // 3]
trending_parks = [
{
"id": 1,
"name": "Cedar Point",
"location": "Sandusky, OH",
"category": "Theme Park",
"rating": 4.6,
"rank": 1,
"views": 45678,
"views_change": "+12%",
"slug": "cedar-point",
},
{
"id": 2,
"name": "Magic Kingdom",
"location": "Orlando, FL",
"category": "Theme Park",
"rating": 4.5,
"rank": 2,
"views": 67890,
"views_change": "+8%",
"slug": "magic-kingdom",
},
][: limit // 3]
latest_reviews = [
{
"id": 1,
"name": "Steel Vengeance Review",
"location": "Cedar Point",
"category": "Roller Coaster",
"rating": 5.0,
"rank": 1,
"views": 1234,
"views_change": "+45%",
"slug": "steel-vengeance-review",
}
][: limit // 3]
response_data = {
"trending_rides": trending_rides,
"trending_parks": trending_parks,
"latest_reviews": latest_reviews,
}
return Response(response_data)
@extend_schema_view(
get=extend_schema(
summary="Get new content",
description="Retrieve recently added parks and rides.",
parameters=[
{
"name": "limit",
"in": "query",
"description": "Number of new items to return (default: 20, max: 100)",
"required": False,
"schema": {"type": "integer", "default": 20, "maximum": 100},
},
{
"name": "days",
"in": "query",
"description": "Number of days to look back for new content (default: 30, max: 365)",
"required": False,
"schema": {"type": "integer", "default": 30, "maximum": 365},
},
],
responses={200: OpenApiTypes.OBJECT},
tags=["Trending"],
),
)
class NewContentAPIView(APIView):
"""API endpoint for new content."""
permission_classes = [AllowAny]
def get(self, request: Request) -> Response:
"""Get new parks and rides."""
try:
from apps.core.services.trending_service import TrendingService
except ImportError:
# Fallback if trending service is not available
return self._get_fallback_new_content(request)
# Parse parameters
limit = min(int(request.query_params.get("limit", 20)), 100)
# Get new content with longer timeframe to get more data
trending_service = TrendingService()
all_new_content = trending_service.get_new_content(
limit=limit * 2, days_back=60
)
recently_added = []
newly_opened = []
upcoming = []
# Categorize items based on date
today = date.today()
for item in all_new_content:
date_added = item.get("date_added", "")
if date_added:
try:
# Parse the date string
if isinstance(date_added, str):
item_date = datetime.fromisoformat(date_added).date()
else:
item_date = date_added
# Calculate days difference
days_diff = (today - item_date).days
if days_diff <= 30: # Recently added (last 30 days)
recently_added.append(item)
elif days_diff <= 365: # Newly opened (last year)
newly_opened.append(item)
else: # Older items
newly_opened.append(item)
except (ValueError, TypeError):
# If date parsing fails, add to recently added
recently_added.append(item)
else:
recently_added.append(item)
# Create mock upcoming items
upcoming = [
{
"id": 1,
"name": "Epic Universe",
"location": "Universal Orlando",
"category": "Theme Park",
"date_added": "Opening 2025",
"slug": "epic-universe",
},
{
"id": 2,
"name": "New Fantasyland Expansion",
"location": "Magic Kingdom",
"category": "Land Expansion",
"date_added": "Opening 2026",
"slug": "fantasyland-expansion",
},
]
# Limit each category
recently_added = recently_added[: limit // 3] if recently_added else []
newly_opened = newly_opened[: limit // 3] if newly_opened else []
upcoming = upcoming[: limit // 3] if upcoming else []
# Return in expected frontend format
response_data = {
"recently_added": recently_added,
"newly_opened": newly_opened,
"upcoming": upcoming,
}
return Response(response_data)
def _get_fallback_new_content(self, request: Request) -> Response:
"""Fallback method when trending service is not available."""
limit = min(int(request.query_params.get("limit", 20)), 100)
# Mock new content data
recently_added = [
{
"id": 1,
"name": "Iron Gwazi",
"location": "Busch Gardens Tampa",
"category": "Roller Coaster",
"date_added": "2024-12-01",
"slug": "iron-gwazi",
},
{
"id": 2,
"name": "VelociCoaster",
"location": "Universal's Islands of Adventure",
"category": "Roller Coaster",
"date_added": "2024-11-15",
"slug": "velocicoaster",
},
][: limit // 3]
newly_opened = [
{
"id": 3,
"name": "Guardians of the Galaxy",
"location": "EPCOT",
"category": "Roller Coaster",
"date_added": "2024-10-01",
"slug": "guardians-galaxy",
},
{
"id": 4,
"name": "TRON Lightcycle Run",
"location": "Magic Kingdom",
"category": "Roller Coaster",
"date_added": "2024-09-15",
"slug": "tron-lightcycle",
},
][: limit // 3]
upcoming = [
{
"id": 5,
"name": "Epic Universe",
"location": "Universal Orlando",
"category": "Theme Park",
"date_added": "Opening 2025",
"slug": "epic-universe",
},
{
"id": 6,
"name": "New Fantasyland Expansion",
"location": "Magic Kingdom",
"category": "Land Expansion",
"date_added": "Opening 2026",
"slug": "fantasyland-expansion",
},
][: limit // 3]
response_data = {
"recently_added": recently_added,
"newly_opened": newly_opened,
"upcoming": upcoming,
}
return Response(response_data)