mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:11:08 -05:00
- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns. - Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability. - Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints. - Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
461 lines
14 KiB
Python
461 lines
14 KiB
Python
"""
|
|
Base Views for Contract-Compliant API Responses
|
|
|
|
This module provides base view classes that ensure all API responses follow
|
|
consistent formats that match frontend TypeScript interfaces exactly.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, Optional, Type
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from rest_framework.serializers import Serializer
|
|
from django.conf import settings
|
|
|
|
from apps.api.v1.serializers.shared import (
|
|
validate_filter_metadata_contract
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ContractCompliantAPIView(APIView):
|
|
"""
|
|
Base API view that ensures all responses are contract-compliant.
|
|
|
|
This view provides:
|
|
- Standardized success response format
|
|
- Consistent error response format
|
|
- Automatic contract validation in DEBUG mode
|
|
- Proper error logging with context
|
|
"""
|
|
|
|
# Override in subclasses to specify response serializer
|
|
response_serializer_class: Optional[Type[Serializer]] = None
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
"""Override dispatch to add contract validation."""
|
|
try:
|
|
response = super().dispatch(request, *args, **kwargs)
|
|
|
|
# Validate contract in DEBUG mode
|
|
if settings.DEBUG and hasattr(response, 'data'):
|
|
self._validate_response_contract(response.data)
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
# Log the error with context
|
|
logger.error(
|
|
f"API error in {self.__class__.__name__}: {str(e)}",
|
|
extra={
|
|
'view_class': self.__class__.__name__,
|
|
'request_path': request.path,
|
|
'request_method': request.method,
|
|
'user': getattr(request, 'user', None),
|
|
'error': str(e)
|
|
},
|
|
exc_info=True
|
|
)
|
|
|
|
# Return standardized error response
|
|
return self.error_response(
|
|
message="An internal error occurred",
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
def success_response(
|
|
self,
|
|
data: Any = None,
|
|
message: str = None,
|
|
status_code: int = status.HTTP_200_OK,
|
|
headers: Dict[str, str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a standardized success response.
|
|
|
|
Args:
|
|
data: Response data
|
|
message: Optional success message
|
|
status_code: HTTP status code
|
|
headers: Optional response headers
|
|
|
|
Returns:
|
|
Response with standardized format
|
|
"""
|
|
response_data = {
|
|
'success': True
|
|
}
|
|
|
|
if data is not None:
|
|
response_data['data'] = data
|
|
|
|
if message:
|
|
response_data['message'] = message
|
|
|
|
return Response(
|
|
response_data,
|
|
status=status_code,
|
|
headers=headers
|
|
)
|
|
|
|
def error_response(
|
|
self,
|
|
message: str,
|
|
status_code: int = status.HTTP_400_BAD_REQUEST,
|
|
error_code: str = None,
|
|
details: Any = None,
|
|
headers: Dict[str, str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a standardized error response.
|
|
|
|
Args:
|
|
message: Error message
|
|
status_code: HTTP status code
|
|
error_code: Optional error code
|
|
details: Optional error details
|
|
headers: Optional response headers
|
|
|
|
Returns:
|
|
Response with standardized error format
|
|
"""
|
|
error_data = {
|
|
'code': error_code or 'API_ERROR',
|
|
'message': message
|
|
}
|
|
|
|
if details:
|
|
error_data['details'] = details
|
|
|
|
# Add user context if available
|
|
if hasattr(self, 'request') and hasattr(self.request, 'user'):
|
|
user = self.request.user
|
|
if user and user.is_authenticated:
|
|
error_data['request_user'] = user.username
|
|
|
|
response_data = {
|
|
'status': 'error',
|
|
'error': error_data,
|
|
'data': None
|
|
}
|
|
|
|
return Response(
|
|
response_data,
|
|
status=status_code,
|
|
headers=headers
|
|
)
|
|
|
|
def validation_error_response(
|
|
self,
|
|
errors: Dict[str, Any],
|
|
message: str = "Validation failed"
|
|
) -> Response:
|
|
"""
|
|
Create a standardized validation error response.
|
|
|
|
Args:
|
|
errors: Validation errors dictionary
|
|
message: Error message
|
|
|
|
Returns:
|
|
Response with validation errors
|
|
"""
|
|
return Response(
|
|
{
|
|
'success': False,
|
|
'message': message,
|
|
'errors': errors
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
def _validate_response_contract(self, data: Any) -> None:
|
|
"""
|
|
Validate response data against expected contracts.
|
|
|
|
This method is called automatically in DEBUG mode to catch
|
|
contract violations during development.
|
|
"""
|
|
try:
|
|
# Check if this looks like filter metadata
|
|
if isinstance(data, dict) and 'categorical' in data and 'ranges' in data:
|
|
validate_filter_metadata_contract(data)
|
|
|
|
# Add more contract validations as needed
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Contract validation failed in {self.__class__.__name__}: {str(e)}",
|
|
extra={
|
|
'view_class': self.__class__.__name__,
|
|
'validation_error': str(e),
|
|
'response_data_type': type(data).__name__
|
|
}
|
|
)
|
|
|
|
|
|
class FilterMetadataAPIView(ContractCompliantAPIView):
|
|
"""
|
|
Base view for filter metadata endpoints.
|
|
|
|
This view ensures filter metadata responses always follow the correct
|
|
contract that matches frontend TypeScript interfaces.
|
|
"""
|
|
|
|
def get_filter_metadata(self) -> Dict[str, Any]:
|
|
"""
|
|
Override this method in subclasses to provide filter metadata.
|
|
|
|
Returns:
|
|
Filter metadata dictionary
|
|
"""
|
|
raise NotImplementedError("Subclasses must implement get_filter_metadata()")
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Handle GET requests for filter metadata."""
|
|
try:
|
|
metadata = self.get_filter_metadata()
|
|
|
|
# Validate the metadata contract
|
|
validated_metadata = validate_filter_metadata_contract(metadata)
|
|
|
|
return self.success_response(validated_metadata)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error getting filter metadata in {self.__class__.__name__}: {str(e)}",
|
|
extra={
|
|
'view_class': self.__class__.__name__,
|
|
'error': str(e)
|
|
},
|
|
exc_info=True
|
|
)
|
|
|
|
return self.error_response(
|
|
message="Failed to retrieve filter metadata",
|
|
error_code="FILTER_METADATA_ERROR"
|
|
)
|
|
|
|
|
|
class HybridFilteringAPIView(ContractCompliantAPIView):
|
|
"""
|
|
Base view for hybrid filtering endpoints.
|
|
|
|
This view provides common functionality for hybrid filtering responses
|
|
and ensures they follow the correct contract.
|
|
"""
|
|
|
|
def get_hybrid_data(self, filters: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
"""
|
|
Override this method in subclasses to provide hybrid data.
|
|
|
|
Args:
|
|
filters: Filter parameters
|
|
|
|
Returns:
|
|
Hybrid response dictionary
|
|
"""
|
|
raise NotImplementedError("Subclasses must implement get_hybrid_data()")
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Handle GET requests for hybrid filtering."""
|
|
try:
|
|
# Extract filters from request parameters
|
|
filters = self.extract_filters(request)
|
|
|
|
# Get hybrid data
|
|
hybrid_data = self.get_hybrid_data(filters)
|
|
|
|
# Validate hybrid response structure
|
|
self._validate_hybrid_response(hybrid_data)
|
|
|
|
return self.success_response(hybrid_data)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error in hybrid filtering for {self.__class__.__name__}: {str(e)}",
|
|
extra={
|
|
'view_class': self.__class__.__name__,
|
|
'filters': getattr(self, '_extracted_filters', {}),
|
|
'error': str(e)
|
|
},
|
|
exc_info=True
|
|
)
|
|
|
|
return self.error_response(
|
|
message="Failed to retrieve filtered data",
|
|
error_code="HYBRID_FILTERING_ERROR"
|
|
)
|
|
|
|
def extract_filters(self, request) -> Dict[str, Any]:
|
|
"""
|
|
Extract filter parameters from request.
|
|
|
|
Override this method in subclasses to customize filter extraction.
|
|
|
|
Args:
|
|
request: HTTP request object
|
|
|
|
Returns:
|
|
Dictionary of filter parameters
|
|
"""
|
|
# Basic implementation - extract all query parameters
|
|
filters = {}
|
|
for key, value in request.query_params.items():
|
|
if value: # Only include non-empty values
|
|
filters[key] = value
|
|
|
|
# Store for error logging
|
|
self._extracted_filters = filters
|
|
|
|
return filters
|
|
|
|
def _validate_hybrid_response(self, data: Dict[str, Any]) -> None:
|
|
"""Validate hybrid response structure."""
|
|
required_fields = ['strategy', 'total_count']
|
|
|
|
for field in required_fields:
|
|
if field not in data:
|
|
raise ValueError(f"Hybrid response missing required field: {field}")
|
|
|
|
# Validate strategy value
|
|
if data['strategy'] not in ['client_side', 'server_side']:
|
|
raise ValueError(f"Invalid strategy value: {data['strategy']}")
|
|
|
|
# Validate filter metadata if present
|
|
if 'filter_metadata' in data:
|
|
validate_filter_metadata_contract(data['filter_metadata'])
|
|
|
|
|
|
class PaginatedAPIView(ContractCompliantAPIView):
|
|
"""
|
|
Base view for paginated responses.
|
|
|
|
This view ensures paginated responses follow the correct contract
|
|
with consistent pagination metadata.
|
|
"""
|
|
|
|
default_page_size = 20
|
|
max_page_size = 100
|
|
|
|
def get_paginated_response(
|
|
self,
|
|
queryset,
|
|
serializer_class: Type[Serializer],
|
|
request,
|
|
page_size: int = None
|
|
) -> Response:
|
|
"""
|
|
Create a paginated response.
|
|
|
|
Args:
|
|
queryset: Django queryset to paginate
|
|
serializer_class: Serializer class for items
|
|
request: HTTP request object
|
|
page_size: Optional page size override
|
|
|
|
Returns:
|
|
Paginated response
|
|
"""
|
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|
|
|
# Determine page size
|
|
if page_size is None:
|
|
page_size = min(
|
|
int(request.query_params.get('page_size', self.default_page_size)),
|
|
self.max_page_size
|
|
)
|
|
|
|
# Get page number
|
|
page_number = request.query_params.get('page', 1)
|
|
|
|
try:
|
|
page_number = int(page_number)
|
|
except (ValueError, TypeError):
|
|
page_number = 1
|
|
|
|
# Create paginator
|
|
paginator = Paginator(queryset, page_size)
|
|
|
|
try:
|
|
page = paginator.page(page_number)
|
|
except PageNotAnInteger:
|
|
page = paginator.page(1)
|
|
except EmptyPage:
|
|
page = paginator.page(paginator.num_pages)
|
|
|
|
# Serialize data
|
|
serializer = serializer_class(page.object_list, many=True)
|
|
|
|
# Build pagination URLs
|
|
request_url = request.build_absolute_uri().split('?')[0]
|
|
query_params = request.query_params.copy()
|
|
|
|
next_url = None
|
|
if page.has_next():
|
|
query_params['page'] = page.next_page_number()
|
|
next_url = f"{request_url}?{query_params.urlencode()}"
|
|
|
|
previous_url = None
|
|
if page.has_previous():
|
|
query_params['page'] = page.previous_page_number()
|
|
previous_url = f"{request_url}?{query_params.urlencode()}"
|
|
|
|
# Create response data
|
|
response_data = {
|
|
'count': paginator.count,
|
|
'next': next_url,
|
|
'previous': previous_url,
|
|
'results': serializer.data,
|
|
'page_size': page_size,
|
|
'current_page': page.number,
|
|
'total_pages': paginator.num_pages
|
|
}
|
|
|
|
return self.success_response(response_data)
|
|
|
|
|
|
def contract_compliant_view(view_class):
|
|
"""
|
|
Decorator to make any view contract-compliant.
|
|
|
|
This decorator can be applied to existing views to add contract
|
|
validation without changing the base class.
|
|
"""
|
|
original_dispatch = view_class.dispatch
|
|
|
|
def new_dispatch(self, request, *args, **kwargs):
|
|
try:
|
|
response = original_dispatch(self, request, *args, **kwargs)
|
|
|
|
# Add contract validation in DEBUG mode
|
|
if settings.DEBUG and hasattr(response, 'data'):
|
|
# Basic validation - can be extended
|
|
pass
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error in decorated view {view_class.__name__}: {str(e)}",
|
|
exc_info=True
|
|
)
|
|
|
|
# Return basic error response
|
|
return Response(
|
|
{
|
|
'status': 'error',
|
|
'error': {
|
|
'code': 'API_ERROR',
|
|
'message': 'An internal error occurred'
|
|
},
|
|
'data': None
|
|
},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
view_class.dispatch = new_dispatch
|
|
return view_class
|