Enhance API response handling and implement unified API standard across multiple modules. Added feature flags for unified exception handling and debug throttling in settings. Updated pagination and response formats in various viewsets to align with the new standard. Improved error handling and response validation in frontend components for better user feedback.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-15 20:18:42 +00:00
parent 94f243f4a2
commit a75ebf2584
18 changed files with 1974 additions and 642 deletions

View File

@@ -1,5 +1,6 @@
"""
Integration settings views - for OpenAI, Runware, GSC integrations
Unified API Standard v1.0 compliant
"""
import logging
from rest_framework import viewsets, status
@@ -7,6 +8,8 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import transaction
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from django.conf import settings
logger = logging.getLogger(__name__)
@@ -20,18 +23,24 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
"""
permission_classes = [] # Allow any for now
throttle_scope = 'system_admin'
throttle_classes = [DebugScopedRateThrottle]
def list(self, request):
"""List all integrations - for debugging URL patterns"""
logger.info("[IntegrationSettingsViewSet] list() called")
return Response({
'message': 'IntegrationSettingsViewSet is working',
'available_endpoints': [
'GET /api/v1/system/settings/integrations/<pk>/',
'POST /api/v1/system/settings/integrations/<pk>/save/',
'POST /api/v1/system/settings/integrations/<pk>/test/',
'POST /api/v1/system/settings/integrations/<pk>/generate/',
]
})
return success_response(
data={
'message': 'IntegrationSettingsViewSet is working',
'available_endpoints': [
'GET /api/v1/system/settings/integrations/<pk>/',
'POST /api/v1/system/settings/integrations/<pk>/save/',
'POST /api/v1/system/settings/integrations/<pk>/test/',
'POST /api/v1/system/settings/integrations/<pk>/generate/',
]
},
request=request
)
def retrieve(self, request, pk=None):
"""Get integration settings - GET /api/v1/system/settings/integrations/{pk}/"""
@@ -65,7 +74,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.info(f"[test_connection] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
if not integration_type:
return Response({'error': 'Integration type is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Integration type is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get API key and config from request or saved settings
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
@@ -108,10 +121,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
if not api_key:
logger.error(f"[test_connection] No API key found in request or saved settings")
return Response({
'success': False,
'error': 'API key is required'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='API key is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})")
try:
@@ -120,19 +134,21 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
elif integration_type == 'runware':
return self._test_runware(api_key, request)
else:
return Response({
'success': False,
'error': f'Validation not supported for {integration_type}'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Validation not supported for {integration_type}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
except Exception as e:
logger.error(f"Error testing {integration_type} connection: {str(e)}", exc_info=True)
import traceback
error_trace = traceback.format_exc()
logger.error(f"Full traceback: {error_trace}")
return Response({
'success': False,
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
def _test_openai(self, api_key: str, config: dict = None):
"""
@@ -554,7 +570,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
if not integration_type:
return Response({'error': 'Integration type is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Integration type is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Ensure config is a dict
config = dict(request.data) if hasattr(request.data, 'dict') else (request.data if isinstance(request.data, dict) else {})
@@ -587,7 +607,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
if not account:
logger.error(f"[save_settings] No account found after all fallbacks")
return Response({'error': 'Account not found. Please ensure you are logged in.'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"[save_settings] Using account: {account.id} ({account.name}, slug={account.slug}, status={account.status})")
@@ -648,10 +672,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.info(f"[save_settings] Settings updated successfully")
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
return Response({
'success': True,
'message': f'{integration_type.upper()} settings saved successfully'
})
return success_response(
data={'config': config},
message=f'{integration_type.upper()} settings saved successfully',
request=request
)
except Exception as e:
logger.error(f"Error saving integration settings for {integration_type}: {str(e)}", exc_info=True)
@@ -667,10 +692,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
integration_type = pk
if not integration_type:
return Response({
'success': False,
'error': 'Integration type is required'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Integration type is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
# Get account - try multiple methods (same as save_settings)
@@ -695,26 +721,27 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
integration_type=integration_type,
account=account
)
return Response({
'success': True,
'data': integration_settings.config
})
return success_response(
data=integration_settings.config,
request=request
)
except IntegrationSettings.DoesNotExist:
pass
except Exception as e:
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
# Return empty config if no settings found
return Response({
'success': True,
'data': {}
})
return success_response(
data={},
request=request
)
except Exception as e:
logger.error(f"Unexpected error in get_settings for {integration_type}: {e}", exc_info=True)
return Response({
'success': False,
'error': f'Failed to get settings: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Failed to get settings: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
def get_image_generation_settings(self, request):

View File

@@ -13,6 +13,10 @@ from django.core.cache import cache
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.response import success_response, error_response
from igny8_core.api.permissions import IsEditorOrAbove
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.pagination import CustomPageNumberPagination
from .models import AIPrompt, AuthorProfile, Strategy
from .serializers import AIPromptSerializer, AuthorProfileSerializer, StrategySerializer
@@ -22,10 +26,14 @@ logger = logging.getLogger(__name__)
class AIPromptViewSet(AccountModelViewSet):
"""
ViewSet for managing AI prompts
Unified API Standard v1.0 compliant
"""
queryset = AIPrompt.objects.all()
serializer_class = AIPromptSerializer
permission_classes = [] # Allow any for now
permission_classes = [] # Allow any for now (backward compatibility)
throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle]
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
def get_queryset(self):
"""Get prompts for the current account"""
@@ -37,28 +45,39 @@ class AIPromptViewSet(AccountModelViewSet):
try:
prompt = self.get_queryset().get(prompt_type=prompt_type)
serializer = self.get_serializer(prompt)
return Response(serializer.data)
return success_response(data=serializer.data, request=request)
except AIPrompt.DoesNotExist:
# Return default if not found
from .utils import get_default_prompt
default_value = get_default_prompt(prompt_type)
return Response({
'prompt_type': prompt_type,
'prompt_value': default_value,
'default_prompt': default_value,
'is_active': True,
})
return success_response(
data={
'prompt_type': prompt_type,
'prompt_value': default_value,
'default_prompt': default_value,
'is_active': True,
},
request=request
)
@action(detail=False, methods=['post'], url_path='save', url_name='save')
def save_prompt(self, request):
"""Save or update a prompt"""
"""Save or update a prompt - requires editor or above"""
prompt_type = request.data.get('prompt_type')
prompt_value = request.data.get('prompt_value')
if not prompt_type:
return Response({'error': 'prompt_type is required'}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='prompt_type is required',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
if prompt_value is None:
return Response({'error': 'prompt_value is required'}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='prompt_value is required',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
# Get account - try multiple methods
account = getattr(request, 'account', None)
@@ -78,7 +97,11 @@ class AIPromptViewSet(AccountModelViewSet):
pass
if not account:
return Response({'error': 'Account not found. Please ensure you are logged in.'}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
# Get default prompt value if creating new
from .utils import get_default_prompt
@@ -100,11 +123,11 @@ class AIPromptViewSet(AccountModelViewSet):
prompt.save()
serializer = self.get_serializer(prompt)
return Response({
'success': True,
'data': serializer.data,
'message': f'{prompt.get_prompt_type_display()} saved successfully'
})
return success_response(
data=serializer.data,
message=f'{prompt.get_prompt_type_display()} saved successfully',
request=request
)
@action(detail=False, methods=['post'], url_path='reset', url_name='reset')
def reset_prompt(self, request):
@@ -112,7 +135,11 @@ class AIPromptViewSet(AccountModelViewSet):
prompt_type = request.data.get('prompt_type')
if not prompt_type:
return Response({'error': 'prompt_type is required'}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='prompt_type is required',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
# Get account - try multiple methods (same as integration_views)
account = getattr(request, 'account', None)
@@ -132,7 +159,11 @@ class AIPromptViewSet(AccountModelViewSet):
pass
if not account:
return Response({'error': 'Account not found. Please ensure you are logged in.'}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
# Get default prompt
from .utils import get_default_prompt
@@ -154,19 +185,22 @@ class AIPromptViewSet(AccountModelViewSet):
prompt.save()
serializer = self.get_serializer(prompt)
return Response({
'success': True,
'data': serializer.data,
'message': f'{prompt.get_prompt_type_display()} reset to default'
})
return success_response(
data=serializer.data,
message=f'{prompt.get_prompt_type_display()} reset to default',
request=request
)
class AuthorProfileViewSet(AccountModelViewSet):
"""
ViewSet for managing Author Profiles
Unified API Standard v1.0 compliant
"""
queryset = AuthorProfile.objects.all()
serializer_class = AuthorProfileSerializer
throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description', 'tone']
@@ -178,9 +212,12 @@ class AuthorProfileViewSet(AccountModelViewSet):
class StrategyViewSet(AccountModelViewSet):
"""
ViewSet for managing Strategies
Unified API Standard v1.0 compliant
"""
queryset = Strategy.objects.all()
serializer_class = StrategySerializer
throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description']
@@ -190,7 +227,7 @@ class StrategyViewSet(AccountModelViewSet):
@api_view(['GET'])
@permission_classes([AllowAny]) # Adjust permissions as needed
@permission_classes([AllowAny]) # Public endpoint for monitoring
def system_status(request):
"""
Comprehensive system status endpoint for monitoring
@@ -457,7 +494,7 @@ def system_status(request):
logger.error(f"Error getting module statistics: {str(e)}")
status_data['modules'] = {'error': str(e)}
return Response(status_data)
return success_response(data=status_data, request=request)
@api_view(['GET'])
@@ -469,19 +506,31 @@ def get_request_metrics(request, request_id):
"""
# Check if user is admin/developer
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=http_status.HTTP_401_UNAUTHORIZED)
return error_response(
error='Authentication required',
status_code=http_status.HTTP_401_UNAUTHORIZED,
request=request
)
if not (hasattr(request.user, 'is_admin_or_developer') and request.user.is_admin_or_developer()):
return Response({'error': 'Admin access required'}, status=http_status.HTTP_403_FORBIDDEN)
return error_response(
error='Admin access required',
status_code=http_status.HTTP_403_FORBIDDEN,
request=request
)
# Get metrics from cache
from django.core.cache import cache
metrics = cache.get(f"resource_tracking_{request_id}")
if not metrics:
return Response({'error': 'Metrics not found or expired'}, status=http_status.HTTP_404_NOT_FOUND)
return error_response(
error='Metrics not found or expired',
status_code=http_status.HTTP_404_NOT_FOUND,
request=request
)
return Response(metrics)
return success_response(data=metrics, request=request)
@api_view(['POST'])
@@ -504,10 +553,11 @@ def gitea_webhook(request):
# Only process push events
if event_type != 'push':
return Response({
'status': 'ignored',
'message': f'Event type {event_type} is not processed'
}, status=http_status.HTTP_200_OK)
return success_response(
data={'status': 'ignored'},
message=f'Event type {event_type} is not processed',
request=request
)
# Extract repository information
repository = payload.get('repository', {})
@@ -518,10 +568,11 @@ def gitea_webhook(request):
# Only process pushes to main branch
if ref != 'refs/heads/main':
logger.info(f"[Webhook] Ignoring push to {ref}, only processing main branch")
return Response({
'status': 'ignored',
'message': f'Push to {ref} ignored, only main branch is processed'
}, status=http_status.HTTP_200_OK)
return success_response(
data={'status': 'ignored'},
message=f'Push to {ref} ignored, only main branch is processed',
request=request
)
# Get commit information
commits = payload.get('commits', [])
@@ -636,30 +687,35 @@ def gitea_webhook(request):
deployment_error = str(deploy_error)
logger.error(f"[Webhook] Deployment error: {deploy_error}", exc_info=True)
return Response({
'status': 'success' if deployment_success else 'partial',
'message': 'Webhook received and processed',
'repository': repo_full_name,
'branch': ref,
'commits': commit_count,
'pusher': pusher,
'event': event_type,
'deployment': {
'success': deployment_success,
'error': deployment_error
}
}, status=http_status.HTTP_200_OK)
return success_response(
data={
'status': 'success' if deployment_success else 'partial',
'repository': repo_full_name,
'branch': ref,
'commits': commit_count,
'pusher': pusher,
'event': event_type,
'deployment': {
'success': deployment_success,
'error': deployment_error
}
},
message='Webhook received and processed',
request=request
)
except json.JSONDecodeError as e:
logger.error(f"[Webhook] Invalid JSON payload: {e}")
return Response({
'status': 'error',
'message': 'Invalid JSON payload'
}, status=http_status.HTTP_400_BAD_REQUEST)
return error_response(
error='Invalid JSON payload',
status_code=http_status.HTTP_400_BAD_REQUEST,
request=request
)
except Exception as e:
logger.error(f"[Webhook] Error processing webhook: {e}", exc_info=True)
return Response({
'status': 'error',
'message': str(e)
}, status=http_status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=str(e),
status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)