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 @@
"""
ViewSets for Billing API
Unified API Standard v1.0 compliant
"""
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
@@ -10,6 +11,8 @@ from datetime import timedelta
from decimal import Decimal
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication
from .models import CreditTransaction, CreditUsageLog
from .serializers import (
@@ -23,9 +26,12 @@ from .exceptions import InsufficientCreditsError
class CreditBalanceViewSet(viewsets.ViewSet):
"""
ViewSet for credit balance operations
Unified API Standard v1.0 compliant
"""
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
throttle_scope = 'billing'
throttle_classes = [DebugScopedRateThrottle]
@action(detail=False, methods=['get'])
def balance(self, request):
@@ -37,9 +43,10 @@ class CreditBalanceViewSet(viewsets.ViewSet):
account = getattr(user, 'account', None)
if not account:
return Response(
{'error': 'Account not found'},
status=status.HTTP_400_BAD_REQUEST
return error_response(
error='Account not found',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get plan credits per month
@@ -63,18 +70,21 @@ class CreditBalanceViewSet(viewsets.ViewSet):
}
serializer = CreditBalanceSerializer(data)
return Response(serializer.data)
return success_response(data=serializer.data, request=request)
class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for credit usage logs
Unified API Standard v1.0 compliant
"""
queryset = CreditUsageLog.objects.all()
serializer_class = CreditUsageLogSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
pagination_class = CustomPageNumberPagination
throttle_scope = 'billing'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = []
@@ -116,9 +126,10 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
account = getattr(user, 'account', None)
if not account:
return Response(
{'error': 'Account not found'},
status=status.HTTP_400_BAD_REQUEST
return error_response(
error='Account not found',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get date range from query params
@@ -192,7 +203,7 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
}
serializer = UsageSummarySerializer(data)
return Response(serializer.data)
return success_response(data=serializer.data, request=request)
@action(detail=False, methods=['get'], url_path='limits', url_name='limits')
def limits(self, request):
@@ -222,12 +233,12 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
if not account:
logger.warning(f'No account found in limits endpoint')
# Return empty limits instead of error - frontend will show "no data" message
return Response({'limits': []})
return success_response(data={'limits': []}, request=request)
plan = account.plan
if not plan:
# Return empty limits instead of error - allows frontend to show "no plan" message
return Response({'limits': []})
return success_response(data={'limits': []}, request=request)
# Import models
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
@@ -430,18 +441,21 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
])
# Return data directly - serializer validation not needed for read-only endpoint
return Response({'limits': limits_data})
return success_response(data={'limits': limits_data}, request=request)
class CreditTransactionViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for credit transaction history
Unified API Standard v1.0 compliant
"""
queryset = CreditTransaction.objects.all()
serializer_class = CreditTransactionSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
pagination_class = CustomPageNumberPagination
throttle_scope = 'billing'
throttle_classes = [DebugScopedRateThrottle]
def get_queryset(self):
"""Get transactions for current account"""

View File

@@ -10,6 +10,8 @@ import json
import time
from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from .models import Keywords, Clusters, ContentIdeas
from .serializers import KeywordSerializer, ContentIdeasSerializer
from .cluster_serializers import ClusterSerializer
@@ -19,11 +21,14 @@ class KeywordViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing keywords with CRUD operations
Provides list, create, retrieve, update, and destroy actions
Unified API Standard v1.0 compliant
"""
queryset = Keywords.objects.all()
serializer_class = KeywordSerializer
permission_classes = [] # Allow any for now
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
throttle_scope = 'planner'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
@@ -121,13 +126,17 @@ class KeywordViewSet(SiteSectorModelViewSet):
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
return success_response(
data=serializer.data,
request=request
)
except Exception as e:
logger.error(f"Error in KeywordViewSet.list(): {type(e).__name__}: {str(e)}", exc_info=True)
return Response({
'error': f'Error loading keywords: {str(e)}',
'type': type(e).__name__
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Error loading keywords: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
def perform_create(self, serializer):
"""Require explicit site_id and sector_id - no defaults."""
@@ -190,12 +199,16 @@ class KeywordViewSet(SiteSectorModelViewSet):
"""Bulk delete keywords"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
deleted_count, _ = queryset.filter(id__in=ids).delete()
return Response({'deleted_count': deleted_count}, status=status.HTTP_200_OK)
return success_response(data={'deleted_count': deleted_count}, request=request)
@action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update')
def bulk_update(self, request):
@@ -204,14 +217,22 @@ class KeywordViewSet(SiteSectorModelViewSet):
status_value = request.data.get('status')
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if not status_value:
return Response({'error': 'No status provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No status provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
updated_count = queryset.filter(id__in=ids).update(status=status_value)
return Response({'updated_count': updated_count}, status=status.HTTP_200_OK)
return success_response(data={'updated_count': updated_count}, request=request)
@action(detail=False, methods=['post'], url_path='bulk_add_from_seed', url_name='bulk_add_from_seed')
def bulk_add_from_seed(self, request):
@@ -223,32 +244,60 @@ class KeywordViewSet(SiteSectorModelViewSet):
sector_id = request.data.get('sector_id')
if not seed_keyword_ids:
return Response({'error': 'No seed keyword IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No seed keyword IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if not site_id:
return Response({'error': 'site_id is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='site_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if not sector_id:
return Response({'error': 'sector_id is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='sector_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
site = Site.objects.get(id=site_id)
sector = Sector.objects.get(id=sector_id)
except (Site.DoesNotExist, Sector.DoesNotExist) as e:
return Response({'error': f'Invalid site or sector: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Invalid site or sector: {str(e)}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Validate sector belongs to site
if sector.site != site:
return Response({'error': 'Sector does not belong to the specified site'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Sector does not belong to the specified site',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get account from site
account = site.account
if not account:
return Response({'error': 'Site has no account assigned'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Site has no account assigned',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get SeedKeywords
seed_keywords = SeedKeyword.objects.filter(id__in=seed_keyword_ids, is_active=True)
if not seed_keywords.exists():
return Response({'error': 'No valid seed keywords found'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No valid seed keywords found',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
created_count = 0
skipped_count = 0
@@ -288,12 +337,14 @@ class KeywordViewSet(SiteSectorModelViewSet):
errors.append(f"Error adding '{seed_keyword.keyword}': {str(e)}")
skipped_count += 1
return Response({
'success': True,
'created': created_count,
'skipped': skipped_count,
'errors': errors[:10] if errors else [] # Limit errors to first 10
}, status=status.HTTP_200_OK)
return success_response(
data={
'created': created_count,
'skipped': skipped_count,
'errors': errors[:10] if errors else [] # Limit errors to first 10
},
request=request
)
@action(detail=False, methods=['get'], url_path='export', url_name='export')
def export(self, request):
@@ -366,11 +417,19 @@ class KeywordViewSet(SiteSectorModelViewSet):
Automatically links keywords to current active site/sector.
"""
if 'file' not in request.FILES:
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No file provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
file = request.FILES['file']
if not file.name.endswith('.csv'):
return Response({'error': 'File must be a CSV'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='File must be a CSV',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
user = getattr(request, 'user', None)
@@ -391,23 +450,43 @@ class KeywordViewSet(SiteSectorModelViewSet):
# Site ID is REQUIRED
if not site_id:
return Response({'error': 'site_id is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='site_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
return Response({'error': f'Site with id {site_id} does not exist'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Site with id {site_id} does not exist',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Sector ID is REQUIRED
if not sector_id:
return Response({'error': 'sector_id is required'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='sector_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
sector = Sector.objects.get(id=sector_id)
if sector.site_id != site_id:
return Response({'error': 'Sector does not belong to the selected site'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Sector does not belong to the selected site',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
except Sector.DoesNotExist:
return Response({'error': f'Sector with id {sector_id} does not exist'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Sector with id {sector_id} does not exist',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get account
account = getattr(request, 'account', None)
@@ -461,17 +540,21 @@ class KeywordViewSet(SiteSectorModelViewSet):
errors.append(f"Row {row_num}: {str(e)}")
continue
return Response({
'success': True,
'imported': imported_count,
'skipped': skipped_count,
'errors': errors[:10] if errors else [] # Limit errors to first 10
}, status=status.HTTP_200_OK)
return success_response(
data={
'imported': imported_count,
'skipped': skipped_count,
'errors': errors[:10] if errors else [] # Limit errors to first 10
},
request=request
)
except Exception as e:
return Response({
'error': f'Failed to parse CSV: {str(e)}'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Failed to parse CSV: {str(e)}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
@action(detail=False, methods=['post'], url_path='auto_cluster', url_name='auto_cluster')
def auto_cluster(self, request):
@@ -497,16 +580,18 @@ class KeywordViewSet(SiteSectorModelViewSet):
# Validate basic input
if not payload['ids']:
return Response({
'success': False,
'error': 'No IDs provided'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if len(payload['ids']) > 20:
return Response({
'success': False,
'error': 'Maximum 20 keywords allowed for clustering'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Maximum 20 keywords allowed for clustering',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Try to queue Celery task
try:
@@ -517,11 +602,11 @@ class KeywordViewSet(SiteSectorModelViewSet):
account_id=account_id
)
logger.info(f"Task queued: {task.id}")
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Clustering started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Clustering started',
request=request
)
else:
# Celery not available - execute synchronously
logger.warning("Celery not available, executing synchronously")
@@ -531,15 +616,16 @@ class KeywordViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
**result
}, status=status.HTTP_200_OK)
return success_response(
data=result,
request=request
)
else:
return Response({
'success': False,
'error': result.get('error', 'Clustering failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Clustering failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except (KombuOperationalError, ConnectionError) as e:
# Broker connection failed - fall back to synchronous execution
logger.warning(f"Celery broker unavailable, falling back to synchronous execution: {str(e)}")
@@ -549,36 +635,42 @@ class KeywordViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
**result
}, status=status.HTTP_200_OK)
return success_response(
data=result,
request=request
)
else:
return Response({
'success': False,
'error': result.get('error', 'Clustering failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Clustering failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
logger.error(f"Error in auto_cluster: {str(e)}", exc_info=True)
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
)
except Exception as e:
logger.error(f"Unexpected error in auto_cluster: {str(e)}", exc_info=True)
return Response({
'success': False,
'error': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Unexpected error: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
class ClusterViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing clusters with CRUD operations
Unified API Standard v1.0 compliant
"""
queryset = Clusters.objects.all()
serializer_class = ClusterSerializer
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
throttle_scope = 'planner'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
@@ -719,12 +811,16 @@ class ClusterViewSet(SiteSectorModelViewSet):
"""Bulk delete clusters"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
deleted_count, _ = queryset.filter(id__in=ids).delete()
return Response({'deleted_count': deleted_count}, status=status.HTTP_200_OK)
return success_response(data={'deleted_count': deleted_count}, request=request)
@action(detail=False, methods=['post'], url_path='auto_generate_ideas', url_name='auto_generate_ideas')
def auto_generate_ideas(self, request):
@@ -749,16 +845,18 @@ class ClusterViewSet(SiteSectorModelViewSet):
# Validate basic input
if not payload['ids']:
return Response({
'success': False,
'error': 'No IDs provided'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if len(payload['ids']) > 10:
return Response({
'success': False,
'error': 'Maximum 10 clusters allowed for idea generation'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Maximum 10 clusters allowed for idea generation',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Try to queue Celery task
try:
@@ -769,11 +867,11 @@ class ClusterViewSet(SiteSectorModelViewSet):
account_id=account_id
)
logger.info(f"Task queued: {task.id}")
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Idea generation started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Idea generation started',
request=request
)
else:
# Celery not available - execute synchronously
logger.warning("Celery not available, executing synchronously")
@@ -783,15 +881,16 @@ class ClusterViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
**result
}, status=status.HTTP_200_OK)
return success_response(
data=result,
request=request
)
else:
return Response({
'success': False,
'error': result.get('error', 'Idea generation failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Idea generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except (KombuOperationalError, ConnectionError) as e:
# Broker connection failed - fall back to synchronous execution
logger.warning(f"Celery broker unavailable, falling back to synchronous execution: {str(e)}")
@@ -801,27 +900,30 @@ class ClusterViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
**result
}, status=status.HTTP_200_OK)
return success_response(
data=result,
request=request
)
else:
return Response({
'success': False,
'error': result.get('error', 'Idea generation failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Idea generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
logger.error(f"Error in auto_generate_ideas: {str(e)}", exc_info=True)
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
)
except Exception as e:
logger.error(f"Unexpected error in auto_generate_ideas: {str(e)}", exc_info=True)
return Response({
'success': False,
'error': f'Unexpected error: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Unexpected error: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
def list(self, request, *args, **kwargs):
"""
@@ -842,16 +944,22 @@ class ClusterViewSet(SiteSectorModelViewSet):
cluster_list = list(queryset)
ClusterSerializer.prefetch_keyword_stats(cluster_list)
serializer = self.get_serializer(cluster_list, many=True)
return Response(serializer.data)
return success_response(
data=serializer.data,
request=request
)
class ContentIdeasViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing content ideas with CRUD operations
Unified API Standard v1.0 compliant
"""
queryset = ContentIdeas.objects.all()
serializer_class = ContentIdeasSerializer
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
pagination_class = CustomPageNumberPagination
throttle_scope = 'planner'
throttle_classes = [DebugScopedRateThrottle] # Explicitly use custom pagination
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
@@ -919,19 +1027,27 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
"""Bulk delete content ideas"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
deleted_count, _ = queryset.filter(id__in=ids).delete()
return Response({'deleted_count': deleted_count}, status=status.HTTP_200_OK)
return success_response(data={'deleted_count': deleted_count}, request=request)
@action(detail=False, methods=['post'], url_path='bulk_queue_to_writer', url_name='bulk_queue_to_writer')
def bulk_queue_to_writer(self, request):
"""Queue ideas to writer by creating Tasks"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
ideas = queryset.filter(id__in=ids, status='new') # Only queue 'new' ideas
@@ -958,11 +1074,13 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
idea.status = 'scheduled'
idea.save()
return Response({
'success': True,
'created_count': len(created_tasks),
'task_ids': created_tasks,
'message': f'Successfully queued {len(created_tasks)} ideas to writer'
}, status=status.HTTP_200_OK)
return success_response(
data={
'created_count': len(created_tasks),
'task_ids': created_tasks,
},
message=f'Successfully queued {len(created_tasks)} ideas to writer',
request=request
)
# REMOVED: generate_idea action - idea generation function removed

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
)

View File

@@ -6,6 +6,8 @@ from django.db import transaction, models
from django.db.models import Q
from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from .models import Tasks, Images, Content
from .serializers import TasksSerializer, ImagesSerializer, ContentSerializer
@@ -13,10 +15,13 @@ from .serializers import TasksSerializer, ImagesSerializer, ContentSerializer
class TasksViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing tasks with CRUD operations
Unified API Standard v1.0 compliant
"""
queryset = Tasks.objects.select_related('content_record')
serializer_class = TasksSerializer
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
throttle_scope = 'writer'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
@@ -84,12 +89,16 @@ class TasksViewSet(SiteSectorModelViewSet):
"""Bulk delete tasks"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
deleted_count, _ = queryset.filter(id__in=ids).delete()
return Response({'deleted_count': deleted_count}, status=status.HTTP_200_OK)
return success_response(data={'deleted_count': deleted_count}, request=request)
@action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update')
def bulk_update(self, request):
@@ -98,14 +107,22 @@ class TasksViewSet(SiteSectorModelViewSet):
status_value = request.data.get('status')
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if not status_value:
return Response({'error': 'No status provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No status provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
updated_count = queryset.filter(id__in=ids).update(status=status_value)
return Response({'updated_count': updated_count}, status=status.HTTP_200_OK)
return success_response(data={'updated_count': updated_count}, request=request)
@action(detail=False, methods=['post'], url_path='auto_generate_content', url_name='auto_generate_content')
def auto_generate_content(self, request):
@@ -120,17 +137,19 @@ class TasksViewSet(SiteSectorModelViewSet):
ids = request.data.get('ids', [])
if not ids:
logger.warning("auto_generate_content: No IDs provided")
return Response({
'error': 'No IDs provided',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if len(ids) > 10:
logger.warning(f"auto_generate_content: Too many IDs provided: {len(ids)}")
return Response({
'error': 'Maximum 10 tasks allowed for content generation',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Maximum 10 tasks allowed for content generation',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
logger.info(f"auto_generate_content: Processing {len(ids)} task IDs: {ids}")
@@ -151,11 +170,11 @@ class TasksViewSet(SiteSectorModelViewSet):
if existing_count == 0:
logger.error(f"auto_generate_content: No tasks found for IDs: {ids}")
return Response({
'error': f'No tasks found for the provided IDs: {ids}',
'type': 'NotFound',
'requested_ids': ids
}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error=f'No tasks found for the provided IDs: {ids}',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
if existing_count < len(ids):
missing_ids = set(ids) - set(existing_ids)
@@ -171,11 +190,11 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error while querying tasks: {str(db_error)}',
'type': 'OperationalError',
'details': 'Failed to retrieve tasks from database. Please check database connection and try again.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Database error while querying tasks: {str(db_error)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# Try to queue Celery task, fall back to synchronous if Celery not available
try:
@@ -192,11 +211,11 @@ class TasksViewSet(SiteSectorModelViewSet):
account_id=account_id
)
logger.info(f"auto_generate_content: Celery task queued successfully: {task.id}")
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Content generation started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Content generation started',
request=request
)
except KombuOperationalError as celery_error:
logger.error("=" * 80)
logger.error("CELERY ERROR: Failed to queue task")
@@ -206,10 +225,11 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': 'Task queue unavailable. Please try again.',
'type': 'QueueError'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return error_response(
error='Task queue unavailable. Please try again.',
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
request=request
)
except Exception as celery_error:
logger.error("=" * 80)
logger.error("CELERY ERROR: Failed to queue task")
@@ -227,16 +247,17 @@ class TasksViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
'tasks_updated': result.get('count', 0),
'message': 'Content generated successfully (synchronous)'
}, status=status.HTTP_200_OK)
return success_response(
data={'tasks_updated': result.get('count', 0)},
message='Content generated successfully (synchronous)',
request=request
)
else:
return Response({
'error': result.get('error', 'Content generation failed'),
'type': 'TaskExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Content generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
else:
# Celery not available - execute synchronously
logger.info(f"auto_generate_content: Executing synchronously (Celery not available)")
@@ -247,17 +268,18 @@ class TasksViewSet(SiteSectorModelViewSet):
)
if result.get('success'):
logger.info(f"auto_generate_content: Synchronous execution successful: {result.get('count', 0)} tasks updated")
return Response({
'success': True,
'tasks_updated': result.get('count', 0),
'message': 'Content generated successfully'
}, status=status.HTTP_200_OK)
return success_response(
data={'tasks_updated': result.get('count', 0)},
message='Content generated successfully',
request=request
)
else:
logger.error(f"auto_generate_content: Synchronous execution failed: {result.get('error', 'Unknown error')}")
return Response({
'error': result.get('error', 'Content generation failed'),
'type': 'TaskExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Content generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except ImportError as import_error:
logger.error(f"auto_generate_content: ImportError - tasks module not available: {str(import_error)}")
@@ -268,21 +290,22 @@ class TasksViewSet(SiteSectorModelViewSet):
updated_count = tasks.update(status='completed', content='[AI content generation not available]')
logger.info(f"auto_generate_content: Updated {updated_count} tasks (AI generation not available)")
return Response({
'updated_count': updated_count,
'message': 'Tasks updated (AI generation not available)'
}, status=status.HTTP_200_OK)
return success_response(
data={'updated_count': updated_count},
message='Tasks updated (AI generation not available)',
request=request
)
except (OperationalError, DatabaseError) as db_error:
logger.error("=" * 80)
logger.error("DATABASE ERROR: Failed to update tasks")
logger.error(f" - Error type: {type(db_error).__name__}")
logger.error(f" - Error message: {str(db_error)}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error while updating tasks: {str(db_error)}',
'type': 'OperationalError',
'details': 'Failed to update tasks in database. Please check database connection.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Database error while updating tasks: {str(db_error)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except (OperationalError, DatabaseError) as db_error:
logger.error("=" * 80)
@@ -293,11 +316,11 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error during content generation: {str(db_error)}',
'type': 'OperationalError',
'details': 'A database operation failed. This may be due to connection issues, constraint violations, or data integrity problems. Check the logs for more details.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Database error during content generation: {str(db_error)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except IntegrityError as integrity_error:
logger.error("=" * 80)
@@ -306,18 +329,19 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Task IDs: {ids}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Data integrity error: {str(integrity_error)}',
'type': 'IntegrityError',
'details': 'The operation violated database constraints. This may indicate missing required relationships or invalid data.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Data integrity error: {str(integrity_error)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except ValidationError as validation_error:
logger.error(f"auto_generate_content: ValidationError: {str(validation_error)}")
return Response({
'error': f'Validation error: {str(validation_error)}',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error=f'Validation error: {str(validation_error)}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
except Exception as e:
logger.error("=" * 80)
@@ -328,11 +352,11 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Unexpected error: {str(e)}',
'type': type(e).__name__,
'details': 'An unexpected error occurred. Please check the logs for more details.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Unexpected error: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as outer_error:
logger.error("=" * 80)
@@ -341,10 +365,11 @@ class TasksViewSet(SiteSectorModelViewSet):
logger.error(f" - Error message: {str(outer_error)}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Critical error: {str(outer_error)}',
'type': type(outer_error).__name__
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Critical error: {str(outer_error)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
class ImagesViewSet(SiteSectorModelViewSet):
@@ -383,30 +408,38 @@ class ImagesViewSet(SiteSectorModelViewSet):
try:
image = Images.objects.get(pk=pk)
except Images.DoesNotExist:
return Response({
'error': 'Image not found'
}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error='Image not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Check if image has a local path
if not image.image_path:
return Response({
'error': 'No local file path available for this image'
}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error='No local file path available for this image',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
file_path = image.image_path
# Verify file exists at the saved path
if not os.path.exists(file_path):
logger.error(f"[serve_image_file] Image {pk} - File not found at saved path: {file_path}")
return Response({
'error': f'Image file not found at: {file_path}'
}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error=f'Image file not found at: {file_path}',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Check if file is readable
if not os.access(file_path, os.R_OK):
return Response({
'error': 'Image file is not readable'
}, status=status.HTTP_403_FORBIDDEN)
return error_response(
error='Image file is not readable',
status_code=status.HTTP_403_FORBIDDEN,
request=request
)
# Determine content type from file extension
import mimetypes
@@ -422,31 +455,45 @@ class ImagesViewSet(SiteSectorModelViewSet):
filename=os.path.basename(file_path)
)
except Exception as e:
return Response({
'error': f'Failed to serve file: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Failed to serve file: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Images.DoesNotExist:
return Response({
'error': 'Image not found'
}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error='Image not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error serving image file: {str(e)}", exc_info=True)
return Response({
'error': f'Failed to serve image: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Failed to serve image: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=False, methods=['post'], url_path='auto_generate', url_name='auto_generate_images')
def auto_generate_images(self, request):
"""Auto-generate images for tasks using AI"""
task_ids = request.data.get('task_ids', [])
if not task_ids:
return Response({'error': 'No task IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No task IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if len(task_ids) > 10:
return Response({'error': 'Maximum 10 tasks allowed for image generation'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Maximum 10 tasks allowed for image generation',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get account
account = getattr(request, 'account', None)
@@ -464,11 +511,11 @@ class ImagesViewSet(SiteSectorModelViewSet):
payload={'ids': task_ids},
account_id=account_id
)
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Image generation started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Image generation started',
request=request
)
else:
# Celery not available - execute synchronously
result = run_ai_task(
@@ -477,33 +524,39 @@ class ImagesViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
'images_created': result.get('count', 0),
'message': result.get('message', 'Image generation completed')
}, status=status.HTTP_200_OK)
return success_response(
data={'images_created': result.get('count', 0)},
message=result.get('message', 'Image generation completed'),
request=request
)
else:
return Response({
'error': result.get('error', 'Image generation failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Image generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except KombuOperationalError as e:
return Response({
'error': 'Task queue unavailable. Please try again.',
'type': 'QueueError'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return error_response(
error='Task queue unavailable. Please try again.',
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
request=request
)
except ImportError:
# Tasks module not available
return Response({
'error': 'Image generation task not available'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return error_response(
error='Image generation task not available',
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
request=request
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error queuing image generation task: {str(e)}", exc_info=True)
return Response({
'error': f'Failed to start image generation: {str(e)}',
'type': 'TaskError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=f'Failed to start image generation: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update')
def bulk_update(self, request):
@@ -518,7 +571,11 @@ class ImagesViewSet(SiteSectorModelViewSet):
status_value = request.data.get('status')
if not status_value:
return Response({'error': 'No status provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No status provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
queryset = self.get_queryset()
@@ -534,13 +591,21 @@ class ImagesViewSet(SiteSectorModelViewSet):
Q(content=content) | Q(task=content.task)
).update(status=status_value)
except Content.DoesNotExist:
return Response({'error': 'Content not found'}, status=status.HTTP_404_NOT_FOUND)
return error_response(
error='Content not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
elif image_ids:
updated_count = queryset.filter(id__in=image_ids).update(status=status_value)
else:
return Response({'error': 'Either content_id or ids must be provided'}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='Either content_id or ids must be provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
return Response({'updated_count': updated_count}, status=status.HTTP_200_OK)
return success_response(data={'updated_count': updated_count}, request=request)
@action(detail=False, methods=['get'], url_path='content_images', url_name='content_images')
def content_images(self, request):
@@ -621,10 +686,13 @@ class ImagesViewSet(SiteSectorModelViewSet):
# Sort by content title
grouped_data.sort(key=lambda x: x['content_title'])
return Response({
'count': len(grouped_data),
'results': grouped_data
}, status=status.HTTP_200_OK)
return success_response(
data={
'count': len(grouped_data),
'results': grouped_data
},
request=request
)
@action(detail=False, methods=['post'], url_path='generate_images', url_name='generate_images')
def generate_images(self, request):
@@ -636,10 +704,11 @@ class ImagesViewSet(SiteSectorModelViewSet):
content_id = request.data.get('content_id')
if not image_ids:
return Response({
'error': 'No image IDs provided',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No image IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
account_id = account.id if account else None
@@ -651,11 +720,11 @@ class ImagesViewSet(SiteSectorModelViewSet):
account_id=account_id,
content_id=content_id
)
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Image generation started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Image generation started',
request=request
)
else:
# Fallback to synchronous execution (for testing)
result = process_image_generation_queue(
@@ -663,21 +732,25 @@ class ImagesViewSet(SiteSectorModelViewSet):
account_id=account_id,
content_id=content_id
)
return Response(result, status=status.HTTP_200_OK)
return success_response(data=result, request=request)
except Exception as e:
logger.error(f"[generate_images] Error: {str(e)}", exc_info=True)
return Response({
'error': str(e),
'type': 'ExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
class ContentViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing task content
Unified API Standard v1.0 compliant
"""
queryset = Content.objects.all()
serializer_class = ContentSerializer
pagination_class = CustomPageNumberPagination
throttle_scope = 'writer'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', 'meta_title', 'primary_keyword']
@@ -702,10 +775,11 @@ class ContentViewSet(SiteSectorModelViewSet):
ids = request.data.get('ids', [])
if not ids:
return Response({
'error': 'No IDs provided',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
return error_response(
error='No IDs provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
account_id = account.id if account else None
@@ -717,11 +791,11 @@ class ContentViewSet(SiteSectorModelViewSet):
payload={'ids': ids},
account_id=account_id
)
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Image prompt generation started'
}, status=status.HTTP_200_OK)
return success_response(
data={'task_id': str(task.id)},
message='Image prompt generation started',
request=request
)
else:
# Fallback to synchronous execution
result = run_ai_task(
@@ -730,19 +804,21 @@ class ContentViewSet(SiteSectorModelViewSet):
account_id=account_id
)
if result.get('success'):
return Response({
'success': True,
'prompts_created': result.get('count', 0),
'message': 'Image prompts generated successfully'
}, status=status.HTTP_200_OK)
return success_response(
data={'prompts_created': result.get('count', 0)},
message='Image prompts generated successfully',
request=request
)
else:
return Response({
'error': result.get('error', 'Image prompt generation failed'),
'type': 'TaskExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=result.get('error', 'Image prompt generation failed'),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
return Response({
'error': str(e),
'type': 'ExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)