mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 07:51:08 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
@@ -1302,15 +1302,22 @@ def get_user_statistics(request):
|
||||
user = request.user
|
||||
|
||||
# Calculate user statistics
|
||||
# TODO(THRILLWIKI-104): Implement full user statistics tracking
|
||||
# See FUTURE_WORK.md - THRILLWIKI-104 for full statistics tracking implementation
|
||||
from apps.parks.models import ParkReview
|
||||
from apps.parks.models.media import ParkPhoto
|
||||
from apps.rides.models import RideReview
|
||||
from apps.rides.models.media import RidePhoto
|
||||
|
||||
# Count photos uploaded by user
|
||||
park_photos_count = ParkPhoto.objects.filter(uploaded_by=user).count()
|
||||
ride_photos_count = RidePhoto.objects.filter(uploaded_by=user).count()
|
||||
total_photos_uploaded = park_photos_count + ride_photos_count
|
||||
|
||||
data = {
|
||||
"parks_visited": ParkReview.objects.filter(user=user).values("park").distinct().count(),
|
||||
"rides_ridden": RideReview.objects.filter(user=user).values("ride").distinct().count(),
|
||||
"reviews_written": ParkReview.objects.filter(user=user).count() + RideReview.objects.filter(user=user).count(),
|
||||
"photos_uploaded": 0, # TODO(THRILLWIKI-105): Implement photo counting
|
||||
"photos_uploaded": total_photos_uploaded,
|
||||
"top_lists_created": TopList.objects.filter(user=user).count(),
|
||||
"member_since": user.date_joined,
|
||||
"last_activity": user.last_login,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""
|
||||
Centralized core API views.
|
||||
Migrated from apps.core.views.entity_search
|
||||
|
||||
Caching Strategy:
|
||||
- QuickEntitySuggestionView: 5 minutes (300s) - autocomplete should be fast and relatively fresh
|
||||
- EntityFuzzySearchView: No caching - POST requests with varying data
|
||||
- EntityNotFoundView: No caching - POST requests with context-specific data
|
||||
"""
|
||||
|
||||
from rest_framework.views import APIView
|
||||
@@ -14,6 +19,7 @@ from apps.core.services.entity_fuzzy_matching import (
|
||||
entity_fuzzy_matcher,
|
||||
EntityType,
|
||||
)
|
||||
from apps.core.decorators.cache_decorators import cache_api_response
|
||||
|
||||
|
||||
class EntityFuzzySearchView(APIView):
|
||||
@@ -275,6 +281,7 @@ class QuickEntitySuggestionView(APIView):
|
||||
summary="Quick entity suggestions",
|
||||
description="Lightweight endpoint for quick entity suggestions (e.g., autocomplete)",
|
||||
)
|
||||
@cache_api_response(timeout=300, key_prefix="entity_suggestions")
|
||||
def get(self, request):
|
||||
"""
|
||||
Get quick entity suggestions.
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
"""
|
||||
Centralized map API views.
|
||||
Migrated from apps.core.views.map_views
|
||||
|
||||
Caching Strategy:
|
||||
- MapLocationsAPIView: 5 minutes (300s) - map data changes infrequently but needs freshness
|
||||
- MapLocationDetailAPIView: 30 minutes (1800s) - detail views are stable
|
||||
- MapSearchAPIView: 5 minutes (300s) - search results should be consistent
|
||||
- MapBoundsAPIView: 5 minutes (300s) - bounds queries are location-specific
|
||||
- MapStatsAPIView: 10 minutes (600s) - stats are aggregated and change slowly
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from django.db.models import Q
|
||||
from django.core.cache import cache
|
||||
from django.contrib.gis.geos import Polygon
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
@@ -23,6 +30,8 @@ from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
from apps.parks.models import Park
|
||||
from apps.rides.models import Ride
|
||||
from apps.core.services.enhanced_cache_service import EnhancedCacheService
|
||||
from apps.core.decorators.cache_decorators import cache_api_response
|
||||
from ..serializers.maps import (
|
||||
MapLocationsResponseSerializer,
|
||||
MapSearchResponseSerializer,
|
||||
@@ -306,21 +315,28 @@ class MapLocationsAPIView(APIView):
|
||||
return {
|
||||
"status": "success",
|
||||
"locations": locations,
|
||||
"clusters": [], # TODO(THRILLWIKI-106): Implement map clustering algorithm
|
||||
"clusters": [], # See FUTURE_WORK.md - THRILLWIKI-106 for implementation plan
|
||||
"bounds": self._calculate_bounds(locations),
|
||||
"total_count": len(locations),
|
||||
"clustered": params["cluster"],
|
||||
}
|
||||
|
||||
def get(self, request: HttpRequest) -> Response:
|
||||
"""Get map locations with optional clustering and filtering."""
|
||||
"""
|
||||
Get map locations with optional clustering and filtering.
|
||||
|
||||
Caching: Uses EnhancedCacheService with 5-minute timeout (300s).
|
||||
Cache key is based on all query parameters for proper invalidation.
|
||||
"""
|
||||
try:
|
||||
params = self._parse_request_parameters(request)
|
||||
cache_key = self._build_cache_key(params)
|
||||
|
||||
# Check cache first
|
||||
cached_result = cache.get(cache_key)
|
||||
# Use EnhancedCacheService for improved caching with monitoring
|
||||
cache_service = EnhancedCacheService()
|
||||
cached_result = cache_service.get_cached_api_response('map_locations', params)
|
||||
if cached_result:
|
||||
logger.debug(f"Cache hit for map_locations with key: {cache_key}")
|
||||
return Response(cached_result)
|
||||
|
||||
# Get location data
|
||||
@@ -331,8 +347,9 @@ class MapLocationsAPIView(APIView):
|
||||
# Build response
|
||||
result = self._build_response(locations, params)
|
||||
|
||||
# Cache result for 5 minutes
|
||||
cache.set(cache_key, result, 300)
|
||||
# Cache result for 5 minutes using EnhancedCacheService
|
||||
cache_service.cache_api_response('map_locations', params, result, timeout=300)
|
||||
logger.debug(f"Cached map_locations result for key: {cache_key}")
|
||||
|
||||
return Response(result)
|
||||
|
||||
@@ -374,10 +391,15 @@ class MapLocationsAPIView(APIView):
|
||||
),
|
||||
)
|
||||
class MapLocationDetailAPIView(APIView):
|
||||
"""API endpoint for getting detailed information about a specific location."""
|
||||
"""
|
||||
API endpoint for getting detailed information about a specific location.
|
||||
|
||||
Caching: 30-minute timeout (1800s) - detail views are stable and change infrequently.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="map_detail")
|
||||
def get(
|
||||
self, request: HttpRequest, location_type: str, location_id: int
|
||||
) -> Response:
|
||||
@@ -471,7 +493,7 @@ class MapLocationDetailAPIView(APIView):
|
||||
obj.opening_date.isoformat() if obj.opening_date else None
|
||||
),
|
||||
},
|
||||
"nearby_locations": [], # TODO(THRILLWIKI-107): Implement nearby locations for parks
|
||||
"nearby_locations": [], # See FUTURE_WORK.md - THRILLWIKI-107
|
||||
}
|
||||
else: # ride
|
||||
data = {
|
||||
@@ -538,7 +560,7 @@ class MapLocationDetailAPIView(APIView):
|
||||
obj.manufacturer.name if obj.manufacturer else None
|
||||
),
|
||||
},
|
||||
"nearby_locations": [], # TODO(THRILLWIKI-107): Implement nearby locations for rides
|
||||
"nearby_locations": [], # See FUTURE_WORK.md - THRILLWIKI-107
|
||||
}
|
||||
|
||||
return Response(
|
||||
@@ -599,10 +621,16 @@ class MapLocationDetailAPIView(APIView):
|
||||
),
|
||||
)
|
||||
class MapSearchAPIView(APIView):
|
||||
"""API endpoint for searching locations by text query."""
|
||||
"""
|
||||
API endpoint for searching locations by text query.
|
||||
|
||||
Caching: 5-minute timeout (300s) - search results should remain consistent
|
||||
but need to reflect new content additions.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=300, key_prefix="map_search")
|
||||
def get(self, request: HttpRequest) -> Response:
|
||||
"""Search locations by text query with pagination."""
|
||||
try:
|
||||
@@ -669,7 +697,7 @@ class MapSearchAPIView(APIView):
|
||||
else ""
|
||||
),
|
||||
},
|
||||
"relevance_score": 1.0, # TODO(THRILLWIKI-108): Implement relevance scoring for search
|
||||
"relevance_score": 1.0, # See FUTURE_WORK.md - THRILLWIKI-108
|
||||
}
|
||||
)
|
||||
|
||||
@@ -722,7 +750,7 @@ class MapSearchAPIView(APIView):
|
||||
else ""
|
||||
),
|
||||
},
|
||||
"relevance_score": 1.0, # TODO(THRILLWIKI-108): Implement relevance scoring for search
|
||||
"relevance_score": 1.0, # See FUTURE_WORK.md - THRILLWIKI-108
|
||||
}
|
||||
)
|
||||
|
||||
@@ -798,10 +826,16 @@ class MapSearchAPIView(APIView):
|
||||
),
|
||||
)
|
||||
class MapBoundsAPIView(APIView):
|
||||
"""API endpoint for getting locations within specific bounds."""
|
||||
"""
|
||||
API endpoint for getting locations within specific bounds.
|
||||
|
||||
Caching: 5-minute timeout (300s) - bounds queries are location-specific
|
||||
and may be repeated during map navigation.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=300, key_prefix="map_bounds")
|
||||
def get(self, request: HttpRequest) -> Response:
|
||||
"""Get locations within specific geographic bounds."""
|
||||
try:
|
||||
@@ -939,10 +973,15 @@ class MapBoundsAPIView(APIView):
|
||||
),
|
||||
)
|
||||
class MapStatsAPIView(APIView):
|
||||
"""API endpoint for getting map service statistics and health information."""
|
||||
"""
|
||||
API endpoint for getting map service statistics and health information.
|
||||
|
||||
Caching: 10-minute timeout (600s) - stats are aggregated and change slowly.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=600, key_prefix="map_stats")
|
||||
def get(self, request: HttpRequest) -> Response:
|
||||
"""Get map service statistics and performance metrics."""
|
||||
try:
|
||||
@@ -955,14 +994,21 @@ class MapStatsAPIView(APIView):
|
||||
).count()
|
||||
total_locations = parks_with_location + rides_with_location
|
||||
|
||||
# Get cache statistics
|
||||
from apps.core.services.enhanced_cache_service import CacheMonitor
|
||||
cache_monitor = CacheMonitor()
|
||||
cache_stats = cache_monitor.get_cache_statistics('map_locations')
|
||||
|
||||
return Response(
|
||||
{
|
||||
"status": "success",
|
||||
"total_locations": total_locations,
|
||||
"parks_with_location": parks_with_location,
|
||||
"rides_with_location": rides_with_location,
|
||||
"cache_hits": 0, # TODO(THRILLWIKI-109): Implement cache statistics tracking
|
||||
"cache_misses": 0, # TODO(THRILLWIKI-109): Implement cache statistics tracking
|
||||
"cache_hits": cache_stats.get('hits', 0),
|
||||
"cache_misses": cache_stats.get('misses', 0),
|
||||
"cache_hit_rate": cache_stats.get('hit_rate', 0.0),
|
||||
"cache_size": cache_stats.get('size', 0),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ Park API views for ThrillWiki API v1.
|
||||
|
||||
This module contains consolidated park photo viewset for the centralized API structure.
|
||||
Enhanced from rogue implementation to maintain full feature parity.
|
||||
|
||||
Caching Strategy:
|
||||
- HybridParkAPIView: 10 minutes (600s) - park lists are queried frequently
|
||||
- ParkFilterMetadataAPIView: 30 minutes (1800s) - filter metadata is stable
|
||||
- ParkPhotoViewSet.list/retrieve: 5 minutes (300s) - photos may be updated
|
||||
- ParkPhotoViewSet.stats: 10 minutes (600s) - stats are aggregated
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -27,6 +33,7 @@ from apps.core.exceptions import (
|
||||
ValidationException,
|
||||
)
|
||||
from apps.core.utils.error_handling import ErrorHandler
|
||||
from apps.core.decorators.cache_decorators import cache_api_response
|
||||
from apps.parks.models import Park, ParkPhoto
|
||||
from apps.parks.services import ParkMediaService
|
||||
from apps.parks.services.hybrid_loader import smart_park_loader
|
||||
@@ -714,10 +721,14 @@ class HybridParkAPIView(APIView):
|
||||
Automatically chooses between client-side and server-side filtering
|
||||
based on data size and complexity. Provides progressive loading
|
||||
for large datasets and complete data for smaller sets.
|
||||
|
||||
Caching: 10-minute timeout (600s) - park lists are queried frequently
|
||||
but need to reflect new additions within reasonable time.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=600, key_prefix="hybrid_parks")
|
||||
def get(self, request):
|
||||
"""Get parks with hybrid filtering strategy."""
|
||||
# Extract filters from query parameters
|
||||
@@ -950,10 +961,14 @@ class ParkFilterMetadataAPIView(APIView):
|
||||
|
||||
Provides information about available filter options and ranges
|
||||
to help build dynamic filter interfaces.
|
||||
|
||||
Caching: 30-minute timeout (1800s) - filter metadata is stable
|
||||
and only changes when new entities are added.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="park_filter_metadata")
|
||||
def get(self, request):
|
||||
"""Get park filter metadata."""
|
||||
# Check if metadata should be scoped to current filters
|
||||
|
||||
@@ -11,6 +11,16 @@ This module implements a "full fat" set of endpoints:
|
||||
Notes:
|
||||
- These views try to use real Django models if available. If the domain models/services
|
||||
are not present, they return a clear 501 response explaining what to wire up.
|
||||
|
||||
Caching Strategy:
|
||||
- RideListCreateAPIView.get: 10 minutes (600s) - ride lists are frequently queried
|
||||
- RideDetailAPIView.get: 30 minutes (1800s) - detail views are stable
|
||||
- FilterOptionsAPIView.get: 30 minutes (1800s) - filter options change rarely
|
||||
- HybridRideAPIView.get: 10 minutes (600s) - ride lists with filters
|
||||
- RideFilterMetadataAPIView.get: 30 minutes (1800s) - metadata is stable
|
||||
- CompanySearchAPIView.get: 10 minutes (600s) - company data is stable
|
||||
- RideModelSearchAPIView.get: 10 minutes (600s) - ride model data is stable
|
||||
- RideSearchSuggestionsAPIView.get: 5 minutes (300s) - suggestions should be fresh
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -33,6 +43,7 @@ from apps.api.v1.serializers.rides import (
|
||||
RideListOutputSerializer,
|
||||
RideUpdateInputSerializer,
|
||||
)
|
||||
from apps.core.decorators.cache_decorators import cache_api_response
|
||||
from apps.rides.services.hybrid_loader import SmartRideLoader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -73,6 +84,13 @@ class StandardResultsSetPagination(PageNumberPagination):
|
||||
|
||||
# --- Ride list & create -----------------------------------------------------
|
||||
class RideListCreateAPIView(APIView):
|
||||
"""
|
||||
API View for listing and creating rides.
|
||||
|
||||
Caching: GET requests are cached for 10 minutes (600s).
|
||||
POST requests bypass cache and invalidate related cache entries.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
@@ -281,6 +299,7 @@ class RideListCreateAPIView(APIView):
|
||||
responses={200: RideListOutputSerializer(many=True)},
|
||||
tags=["Rides"],
|
||||
)
|
||||
@cache_api_response(timeout=600, key_prefix="ride_list")
|
||||
def get(self, request: Request) -> Response:
|
||||
"""List rides with comprehensive filtering and pagination."""
|
||||
if not MODELS_AVAILABLE:
|
||||
@@ -658,6 +677,13 @@ class RideListCreateAPIView(APIView):
|
||||
tags=["Rides"],
|
||||
)
|
||||
class RideDetailAPIView(APIView):
|
||||
"""
|
||||
API View for retrieving, updating, or deleting a single ride.
|
||||
|
||||
Caching: GET requests are cached for 30 minutes (1800s).
|
||||
PATCH/PUT/DELETE requests bypass cache and should trigger cache invalidation.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def _get_ride_or_404(self, pk: int) -> Any:
|
||||
@@ -671,6 +697,7 @@ class RideDetailAPIView(APIView):
|
||||
except Ride.DoesNotExist: # type: ignore
|
||||
raise NotFound("Ride not found")
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="ride_detail")
|
||||
def get(self, request: Request, pk: int) -> Response:
|
||||
ride = self._get_ride_or_404(pk)
|
||||
serializer = RideDetailOutputSerializer(ride, context={"request": request})
|
||||
@@ -743,8 +770,16 @@ class RideDetailAPIView(APIView):
|
||||
tags=["Rides"],
|
||||
)
|
||||
class FilterOptionsAPIView(APIView):
|
||||
"""
|
||||
API View for ride filter options.
|
||||
|
||||
Caching: 30-minute timeout (1800s) - filter options change rarely
|
||||
and are expensive to compute.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="ride_filter_options")
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return comprehensive filter options with Rich Choice Objects metadata."""
|
||||
# Import Rich Choice registry
|
||||
@@ -1733,8 +1768,13 @@ class FilterOptionsAPIView(APIView):
|
||||
tags=["Rides"],
|
||||
)
|
||||
class CompanySearchAPIView(APIView):
|
||||
"""
|
||||
Caching: 10-minute timeout (600s) - company data is stable.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=600, key_prefix="company_search")
|
||||
def get(self, request: Request) -> Response:
|
||||
q = request.query_params.get("q", "")
|
||||
if not q:
|
||||
@@ -1767,8 +1807,13 @@ class CompanySearchAPIView(APIView):
|
||||
tags=["Rides"],
|
||||
)
|
||||
class RideModelSearchAPIView(APIView):
|
||||
"""
|
||||
Caching: 10-minute timeout (600s) - ride model data is stable.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=600, key_prefix="ride_model_search")
|
||||
def get(self, request: Request) -> Response:
|
||||
q = request.query_params.get("q", "")
|
||||
if not q:
|
||||
@@ -1805,8 +1850,13 @@ class RideModelSearchAPIView(APIView):
|
||||
tags=["Rides"],
|
||||
)
|
||||
class RideSearchSuggestionsAPIView(APIView):
|
||||
"""
|
||||
Caching: 5-minute timeout (300s) - suggestions should be relatively fresh.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=300, key_prefix="ride_suggestions")
|
||||
def get(self, request: Request) -> Response:
|
||||
q = request.query_params.get("q", "")
|
||||
if not q:
|
||||
@@ -2048,10 +2098,14 @@ class HybridRideAPIView(APIView):
|
||||
Automatically chooses between client-side and server-side filtering
|
||||
based on data size and complexity. Provides progressive loading
|
||||
for large datasets and complete data for smaller sets.
|
||||
|
||||
Caching: 10-minute timeout (600s) - ride lists are frequently queried
|
||||
but need to reflect new additions within reasonable time.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=600, key_prefix="hybrid_rides")
|
||||
def get(self, request):
|
||||
"""Get rides with hybrid filtering strategy."""
|
||||
try:
|
||||
@@ -2367,10 +2421,14 @@ class RideFilterMetadataAPIView(APIView):
|
||||
|
||||
Provides information about available filter options and ranges
|
||||
to help build dynamic filter interfaces.
|
||||
|
||||
Caching: 30-minute timeout (1800s) - filter metadata is stable
|
||||
and only changes when new entities are added.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="ride_filter_metadata")
|
||||
def get(self, request):
|
||||
"""Get ride filter metadata."""
|
||||
try:
|
||||
|
||||
@@ -365,7 +365,7 @@ class MapLocationDetailSerializer(serializers.Serializer):
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_nearby_locations(self, obj) -> list:
|
||||
"""Get nearby locations (placeholder for now)."""
|
||||
# TODO(THRILLWIKI-107): Implement nearby location logic using spatial queries
|
||||
# See FUTURE_WORK.md - THRILLWIKI-107 for implementation plan
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# flake8: noqa
|
||||
"""
|
||||
Backup file intentionally cleared to avoid duplicate serializer exports.
|
||||
Original contents were merged into backend/apps/api/v1/auth/serializers.py.
|
||||
This placeholder prevents lint errors while preserving file path for history.
|
||||
"""
|
||||
Reference in New Issue
Block a user