mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:47:04 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -39,7 +39,7 @@ class ContractCompliantAPIView(APIView):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
# Validate contract in DEBUG mode
|
||||
if settings.DEBUG and hasattr(response, 'data'):
|
||||
if settings.DEBUG and hasattr(response, "data"):
|
||||
self._validate_response_contract(response.data)
|
||||
|
||||
return response
|
||||
@@ -49,19 +49,18 @@ class ContractCompliantAPIView(APIView):
|
||||
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)
|
||||
"view_class": self.__class__.__name__,
|
||||
"request_path": request.path,
|
||||
"request_method": request.method,
|
||||
"user": getattr(request, "user", None),
|
||||
"detail": str(e),
|
||||
},
|
||||
exc_info=True
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Return standardized error response
|
||||
return self.error_response(
|
||||
message="An internal error occurred",
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
message="An internal error occurred", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def success_response(
|
||||
@@ -69,7 +68,7 @@ class ContractCompliantAPIView(APIView):
|
||||
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.
|
||||
@@ -83,21 +82,15 @@ class ContractCompliantAPIView(APIView):
|
||||
Returns:
|
||||
Response with standardized format
|
||||
"""
|
||||
response_data = {
|
||||
'success': True
|
||||
}
|
||||
response_data = {"success": True}
|
||||
|
||||
if data is not None:
|
||||
response_data['data'] = data
|
||||
response_data["data"] = data
|
||||
|
||||
if message:
|
||||
response_data['message'] = message
|
||||
response_data["message"] = message
|
||||
|
||||
return Response(
|
||||
response_data,
|
||||
status=status_code,
|
||||
headers=headers
|
||||
)
|
||||
return Response(response_data, status=status_code, headers=headers)
|
||||
|
||||
def error_response(
|
||||
self,
|
||||
@@ -105,7 +98,7 @@ class ContractCompliantAPIView(APIView):
|
||||
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.
|
||||
@@ -120,37 +113,22 @@ class ContractCompliantAPIView(APIView):
|
||||
Returns:
|
||||
Response with standardized error format
|
||||
"""
|
||||
error_data = {
|
||||
'code': error_code or 'API_ERROR',
|
||||
'message': message
|
||||
}
|
||||
error_data = {"code": error_code or "API_ERROR", "message": message}
|
||||
|
||||
if details:
|
||||
error_data['details'] = details
|
||||
error_data["details"] = details
|
||||
|
||||
# Add user context if available
|
||||
if hasattr(self, 'request') and hasattr(self.request, 'user'):
|
||||
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
|
||||
error_data["request_user"] = user.username
|
||||
|
||||
response_data = {
|
||||
'status': 'error',
|
||||
'error': error_data,
|
||||
'data': None
|
||||
}
|
||||
response_data = {"status": "error", "error": error_data, "data": None}
|
||||
|
||||
return Response(
|
||||
response_data,
|
||||
status=status_code,
|
||||
headers=headers
|
||||
)
|
||||
return Response(response_data, status=status_code, headers=headers)
|
||||
|
||||
def validation_error_response(
|
||||
self,
|
||||
errors: dict[str, Any],
|
||||
message: str = "Validation failed"
|
||||
) -> Response:
|
||||
def validation_error_response(self, errors: dict[str, Any], message: str = "Validation failed") -> Response:
|
||||
"""
|
||||
Create a standardized validation error response.
|
||||
|
||||
@@ -161,14 +139,7 @@ class ContractCompliantAPIView(APIView):
|
||||
Returns:
|
||||
Response with validation errors
|
||||
"""
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': message,
|
||||
'errors': errors
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"success": False, "message": message, "errors": errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def _validate_response_contract(self, data: Any) -> None:
|
||||
"""
|
||||
@@ -179,7 +150,7 @@ class ContractCompliantAPIView(APIView):
|
||||
"""
|
||||
try:
|
||||
# Check if this looks like filter metadata
|
||||
if isinstance(data, dict) and 'categorical' in data and 'ranges' in data:
|
||||
if isinstance(data, dict) and "categorical" in data and "ranges" in data:
|
||||
validate_filter_metadata_contract(data)
|
||||
|
||||
# Add more contract validations as needed
|
||||
@@ -188,10 +159,10 @@ class ContractCompliantAPIView(APIView):
|
||||
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__
|
||||
}
|
||||
"view_class": self.__class__.__name__,
|
||||
"validation_error": str(e),
|
||||
"response_data_type": type(data).__name__,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -225,17 +196,11 @@ class FilterMetadataAPIView(ContractCompliantAPIView):
|
||||
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
|
||||
extra={"view_class": self.__class__.__name__, "detail": str(e)},
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return self.error_response(
|
||||
message="Failed to retrieve filter metadata",
|
||||
error_code="FILTER_METADATA_ERROR"
|
||||
)
|
||||
return self.error_response(message="Failed to retrieve filter metadata", error_code="FILTER_METADATA_ERROR")
|
||||
|
||||
|
||||
class HybridFilteringAPIView(ContractCompliantAPIView):
|
||||
@@ -276,17 +241,14 @@ class HybridFilteringAPIView(ContractCompliantAPIView):
|
||||
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)
|
||||
"view_class": self.__class__.__name__,
|
||||
"filters": getattr(self, "_extracted_filters", {}),
|
||||
"detail": str(e),
|
||||
},
|
||||
exc_info=True
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return self.error_response(
|
||||
message="Failed to retrieve filtered data",
|
||||
error_code="HYBRID_FILTERING_ERROR"
|
||||
)
|
||||
return self.error_response(message="Failed to retrieve filtered data", error_code="HYBRID_FILTERING_ERROR")
|
||||
|
||||
def extract_filters(self, request) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -313,19 +275,19 @@ class HybridFilteringAPIView(ContractCompliantAPIView):
|
||||
|
||||
def _validate_hybrid_response(self, data: dict[str, Any]) -> None:
|
||||
"""Validate hybrid response structure."""
|
||||
required_fields = ['strategy', 'total_count']
|
||||
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']:
|
||||
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'])
|
||||
if "filter_metadata" in data:
|
||||
validate_filter_metadata_contract(data["filter_metadata"])
|
||||
|
||||
|
||||
class PaginatedAPIView(ContractCompliantAPIView):
|
||||
@@ -340,11 +302,7 @@ class PaginatedAPIView(ContractCompliantAPIView):
|
||||
max_page_size = 100
|
||||
|
||||
def get_paginated_response(
|
||||
self,
|
||||
queryset,
|
||||
serializer_class: type[Serializer],
|
||||
request,
|
||||
page_size: int = None
|
||||
self, queryset, serializer_class: type[Serializer], request, page_size: int = None
|
||||
) -> Response:
|
||||
"""
|
||||
Create a paginated response.
|
||||
@@ -362,13 +320,10 @@ class PaginatedAPIView(ContractCompliantAPIView):
|
||||
|
||||
# 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
|
||||
)
|
||||
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)
|
||||
page_number = request.query_params.get("page", 1)
|
||||
|
||||
try:
|
||||
page_number = int(page_number)
|
||||
@@ -389,28 +344,28 @@ class PaginatedAPIView(ContractCompliantAPIView):
|
||||
serializer = serializer_class(page.object_list, many=True)
|
||||
|
||||
# Build pagination URLs
|
||||
request_url = request.build_absolute_uri().split('?')[0]
|
||||
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()
|
||||
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()
|
||||
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
|
||||
"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)
|
||||
@@ -430,29 +385,23 @@ def contract_compliant_view(view_class):
|
||||
response = original_dispatch(self, request, *args, **kwargs)
|
||||
|
||||
# Add contract validation in DEBUG mode
|
||||
if settings.DEBUG and hasattr(response, 'data'):
|
||||
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
|
||||
)
|
||||
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": "error",
|
||||
"error": {"code": "API_ERROR", "detail": "An internal error occurred"},
|
||||
"data": None,
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
view_class.dispatch = new_dispatch
|
||||
|
||||
Reference in New Issue
Block a user