section2-3

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-14 17:27:58 +00:00
parent d14d6093e0
commit 5cc4d07373
2 changed files with 414 additions and 296 deletions

View File

@@ -10,6 +10,7 @@ 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 .models import Keywords, Clusters, ContentIdeas
from .serializers import KeywordSerializer, ContentIdeasSerializer
from .cluster_serializers import ClusterSerializer
@@ -124,10 +125,10 @@ class KeywordViewSet(SiteSectorModelViewSet):
return Response(serializer.data)
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
)
def perform_create(self, serializer):
"""Require explicit site_id and sector_id - no defaults."""
@@ -190,12 +191,18 @@ 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
)
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},
message=f'Successfully deleted {deleted_count} keyword(s)'
)
@action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update')
def bulk_update(self, request):
@@ -204,14 +211,23 @@ 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
)
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
)
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},
message=f'Successfully updated {updated_count} keyword(s)'
)
@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 +239,53 @@ 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
)
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
)
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
)
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
)
# 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
)
# 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
)
# 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
)
created_count = 0
skipped_count = 0
@@ -288,12 +325,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
},
message=f'Successfully added {created_count} keyword(s) to workflow'
)
@action(detail=False, methods=['get'], url_path='export', url_name='export')
def export(self, request):
@@ -366,11 +405,17 @@ 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
)
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
)
user = getattr(request, 'user', None)
@@ -391,23 +436,38 @@ 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
)
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
)
# 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
)
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
)
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
)
# Get account
account = getattr(request, 'account', None)
@@ -461,17 +521,20 @@ 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
},
message=f'Successfully imported {imported_count} keyword(s)'
)
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
)
@action(detail=False, methods=['post'], url_path='auto_cluster', url_name='auto_cluster')
def auto_cluster(self, request):
@@ -497,16 +560,16 @@ 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
)
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
)
# Try to queue Celery task
try:
@@ -517,11 +580,12 @@ 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'
)
else:
# Celery not available - execute synchronously
logger.warning("Celery not available, executing synchronously")
@@ -531,15 +595,15 @@ 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,
message='Clustering completed successfully'
)
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
)
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,27 +613,27 @@ 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,
message='Clustering completed successfully'
)
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
)
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
)
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
)
class ClusterViewSet(SiteSectorModelViewSet):
@@ -719,12 +783,18 @@ 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
)
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},
message=f'Successfully deleted {deleted_count} cluster(s)'
)
@action(detail=False, methods=['post'], url_path='auto_generate_ideas', url_name='auto_generate_ideas')
def auto_generate_ideas(self, request):
@@ -749,16 +819,16 @@ 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
)
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
)
# Try to queue Celery task
try:
@@ -769,11 +839,12 @@ 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'
)
else:
# Celery not available - execute synchronously
logger.warning("Celery not available, executing synchronously")
@@ -783,15 +854,15 @@ 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,
message='Idea generation completed successfully'
)
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
)
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 +872,27 @@ 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,
message='Idea generation completed successfully'
)
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
)
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
)
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
)
def list(self, request, *args, **kwargs):
"""
@@ -919,19 +990,28 @@ 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
)
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},
message=f'Successfully deleted {deleted_count} content idea(s)'
)
@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
)
queryset = self.get_queryset()
ideas = queryset.filter(id__in=ids, status='new') # Only queue 'new' ideas
@@ -958,11 +1038,12 @@ 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'
)
# REMOVED: generate_idea action - idea generation function removed