feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -6,16 +6,15 @@ 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 typing import Any
from apps.api.v1.serializers.shared import (
validate_filter_metadata_contract
)
from django.conf import settings
from rest_framework import status
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.views import APIView
from apps.api.v1.serializers.shared import validate_filter_metadata_contract
logger = logging.getLogger(__name__)
@@ -23,28 +22,28 @@ 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
response_serializer_class: type[Serializer] | None = 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(
@@ -58,66 +57,66 @@ class ContractCompliantAPIView(APIView):
},
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,
self,
data: Any = None,
message: str = None,
status_code: int = status.HTTP_200_OK,
headers: Dict[str, str] = None
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,
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
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
"""
@@ -125,40 +124,40 @@ class ContractCompliantAPIView(APIView):
'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],
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
"""
@@ -170,11 +169,11 @@ class ContractCompliantAPIView(APIView):
},
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.
"""
@@ -182,9 +181,9 @@ class ContractCompliantAPIView(APIView):
# 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)}",
@@ -199,30 +198,30 @@ class ContractCompliantAPIView(APIView):
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]:
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)}",
@@ -232,7 +231,7 @@ class FilterMetadataAPIView(ContractCompliantAPIView):
},
exc_info=True
)
return self.error_response(
message="Failed to retrieve filter metadata",
error_code="FILTER_METADATA_ERROR"
@@ -242,37 +241,37 @@ class FilterMetadataAPIView(ContractCompliantAPIView):
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]:
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)}",
@@ -283,21 +282,21 @@ class HybridFilteringAPIView(ContractCompliantAPIView):
},
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]:
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
"""
@@ -306,24 +305,24 @@ class HybridFilteringAPIView(ContractCompliantAPIView):
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:
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'])
@@ -332,77 +331,77 @@ class HybridFilteringAPIView(ContractCompliantAPIView):
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],
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
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
# 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,
@@ -413,36 +412,36 @@ class PaginatedAPIView(ContractCompliantAPIView):
'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(
{
@@ -455,6 +454,6 @@ def contract_compliant_view(view_class):
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
view_class.dispatch = new_dispatch
return view_class