mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
51
backend/apps/api/v1/views/__init__.py
Normal file
51
backend/apps/api/v1/views/__init__.py
Normal 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",
|
||||
]
|
||||
468
backend/apps/api/v1/views/auth.py
Normal file
468
backend/apps/api/v1/views/auth.py
Normal 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)
|
||||
351
backend/apps/api/v1/views/health.py
Normal file
351
backend/apps/api/v1/views/health.py
Normal 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)
|
||||
364
backend/apps/api/v1/views/trending.py
Normal file
364
backend/apps/api/v1/views/trending.py
Normal 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)
|
||||
Reference in New Issue
Block a user