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:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -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),
}
)

View File

@@ -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

View File

@@ -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:

View File

@@ -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 []

View File

@@ -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.
"""