Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -3,15 +3,21 @@ Custom exception handling for ThrillWiki API.
Provides standardized error responses following Django styleguide patterns.
"""
import logging
from typing import Any, Dict, Optional
from django.http import Http404
from django.core.exceptions import PermissionDenied, ValidationError as DjangoValidationError
from django.core.exceptions import (
PermissionDenied,
ValidationError as DjangoValidationError,
)
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError as DRFValidationError, NotFound, PermissionDenied as DRFPermissionDenied
from rest_framework.exceptions import (
ValidationError as DRFValidationError,
NotFound,
PermissionDenied as DRFPermissionDenied,
)
from ..exceptions import ThrillWikiException
from ..logging import get_logger, log_exception
@@ -19,106 +25,133 @@ from ..logging import get_logger, log_exception
logger = get_logger(__name__)
def custom_exception_handler(exc: Exception, context: Dict[str, Any]) -> Optional[Response]:
def custom_exception_handler(
exc: Exception, context: Dict[str, Any]
) -> Optional[Response]:
"""
Custom exception handler for DRF that provides standardized error responses.
Returns:
Response with standardized error format or None to fallback to default handler
"""
# Call REST framework's default exception handler first
response = exception_handler(exc, context)
if response is not None:
# Standardize the error response format
custom_response_data = {
'status': 'error',
'error': {
'code': _get_error_code(exc),
'message': _get_error_message(exc, response.data),
'details': _get_error_details(exc, response.data),
"status": "error",
"error": {
"code": _get_error_code(exc),
"message": _get_error_message(exc, response.data),
"details": _get_error_details(exc, response.data),
},
'data': None,
"data": None,
}
# Add request context for debugging
if hasattr(context.get('request'), 'user'):
custom_response_data['error']['request_user'] = str(context['request'].user)
if hasattr(context.get("request"), "user"):
custom_response_data["error"]["request_user"] = str(context["request"].user)
# Log the error for monitoring
log_exception(logger, exc, context={'response_status': response.status_code}, request=context.get('request'))
log_exception(
logger,
exc,
context={"response_status": response.status_code},
request=context.get("request"),
)
response.data = custom_response_data
# Handle ThrillWiki custom exceptions
elif isinstance(exc, ThrillWikiException):
custom_response_data = {
'status': 'error',
'error': exc.to_dict(),
'data': None,
"status": "error",
"error": exc.to_dict(),
"data": None,
}
log_exception(logger, exc, context={'response_status': exc.status_code}, request=context.get('request'))
log_exception(
logger,
exc,
context={"response_status": exc.status_code},
request=context.get("request"),
)
response = Response(custom_response_data, status=exc.status_code)
# Handle specific Django exceptions that DRF doesn't catch
elif isinstance(exc, DjangoValidationError):
custom_response_data = {
'status': 'error',
'error': {
'code': 'VALIDATION_ERROR',
'message': 'Validation failed',
'details': _format_django_validation_errors(exc),
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": _format_django_validation_errors(exc),
},
'data': None,
"data": None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_400_BAD_REQUEST}, request=context.get('request'))
log_exception(
logger,
exc,
context={"response_status": status.HTTP_400_BAD_REQUEST},
request=context.get("request"),
)
response = Response(custom_response_data, status=status.HTTP_400_BAD_REQUEST)
elif isinstance(exc, Http404):
custom_response_data = {
'status': 'error',
'error': {
'code': 'NOT_FOUND',
'message': 'Resource not found',
'details': str(exc) if str(exc) else None,
"status": "error",
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"details": str(exc) if str(exc) else None,
},
'data': None,
"data": None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_404_NOT_FOUND}, request=context.get('request'))
log_exception(
logger,
exc,
context={"response_status": status.HTTP_404_NOT_FOUND},
request=context.get("request"),
)
response = Response(custom_response_data, status=status.HTTP_404_NOT_FOUND)
elif isinstance(exc, PermissionDenied):
custom_response_data = {
'status': 'error',
'error': {
'code': 'PERMISSION_DENIED',
'message': 'Permission denied',
'details': str(exc) if str(exc) else None,
"status": "error",
"error": {
"code": "PERMISSION_DENIED",
"message": "Permission denied",
"details": str(exc) if str(exc) else None,
},
'data': None,
"data": None,
}
log_exception(logger, exc, context={'response_status': status.HTTP_403_FORBIDDEN}, request=context.get('request'))
log_exception(
logger,
exc,
context={"response_status": status.HTTP_403_FORBIDDEN},
request=context.get("request"),
)
response = Response(custom_response_data, status=status.HTTP_403_FORBIDDEN)
return response
def _get_error_code(exc: Exception) -> str:
"""Extract or determine error code from exception."""
if hasattr(exc, 'default_code'):
if hasattr(exc, "default_code"):
return exc.default_code.upper()
if isinstance(exc, DRFValidationError):
return 'VALIDATION_ERROR'
return "VALIDATION_ERROR"
elif isinstance(exc, NotFound):
return 'NOT_FOUND'
return "NOT_FOUND"
elif isinstance(exc, DRFPermissionDenied):
return 'PERMISSION_DENIED'
return "PERMISSION_DENIED"
return exc.__class__.__name__.upper()
@@ -126,47 +159,47 @@ def _get_error_message(exc: Exception, response_data: Any) -> str:
"""Extract user-friendly error message."""
if isinstance(response_data, dict):
# Handle DRF validation errors
if 'detail' in response_data:
return str(response_data['detail'])
elif 'non_field_errors' in response_data:
errors = response_data['non_field_errors']
if "detail" in response_data:
return str(response_data["detail"])
elif "non_field_errors" in response_data:
errors = response_data["non_field_errors"]
return errors[0] if isinstance(errors, list) and errors else str(errors)
elif isinstance(response_data, dict) and len(response_data) == 1:
key, value = next(iter(response_data.items()))
if isinstance(value, list) and value:
return f"{key}: {value[0]}"
return f"{key}: {value}"
# Fallback to exception message
return str(exc) if str(exc) else 'An error occurred'
return str(exc) if str(exc) else "An error occurred"
def _get_error_details(exc: Exception, response_data: Any) -> Optional[Dict[str, Any]]:
"""Extract detailed error information for debugging."""
if isinstance(response_data, dict) and len(response_data) > 1:
return response_data
if hasattr(exc, 'detail') and isinstance(exc.detail, dict):
if hasattr(exc, "detail") and isinstance(exc.detail, dict):
return exc.detail
return None
def _format_django_validation_errors(exc: DjangoValidationError) -> Dict[str, Any]:
def _format_django_validation_errors(
exc: DjangoValidationError,
) -> Dict[str, Any]:
"""Format Django ValidationError for API response."""
if hasattr(exc, 'error_dict'):
if hasattr(exc, "error_dict"):
# Field-specific errors
return {
field: [str(error) for error in errors]
for field, errors in exc.error_dict.items()
}
elif hasattr(exc, 'error_list'):
elif hasattr(exc, "error_list"):
# Non-field errors
return {
'non_field_errors': [str(error) for error in exc.error_list]
}
return {'non_field_errors': [str(exc)]}
return {"non_field_errors": [str(error) for error in exc.error_list]}
return {"non_field_errors": [str(exc)]}
# Removed _log_api_error - using centralized logging instead