""" 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, ApiResponseSerializer, ErrorResponseSerializer ) 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