from rest_framework import viewsets, filters, status from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from django.db import transaction from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.pagination import CustomPageNumberPagination from .models import Tasks, Images, Content from .serializers import TasksSerializer, ImagesSerializer, ContentSerializer class TasksViewSet(SiteSectorModelViewSet): """ ViewSet for managing tasks with CRUD operations """ queryset = Tasks.objects.select_related('content_record') serializer_class = TasksSerializer pagination_class = CustomPageNumberPagination # Explicitly use custom pagination # DRF filtering configuration filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] # Search configuration search_fields = ['title', 'keywords'] # Ordering configuration ordering_fields = ['title', 'created_at', 'word_count', 'status'] ordering = ['-created_at'] # Default ordering (newest first) # Filter configuration filterset_fields = ['status', 'cluster_id', 'content_type', 'content_structure'] def perform_create(self, serializer): """Require explicit site_id and sector_id - no defaults.""" user = getattr(self.request, 'user', None) try: query_params = getattr(self.request, 'query_params', None) if query_params is None: query_params = getattr(self.request, 'GET', {}) except AttributeError: query_params = {} site_id = serializer.validated_data.get('site_id') or query_params.get('site_id') sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id') from igny8_core.auth.models import Site, Sector from rest_framework.exceptions import ValidationError # Site ID is REQUIRED if not site_id: raise ValidationError("site_id is required. Please select a site.") try: site = Site.objects.get(id=site_id) except Site.DoesNotExist: raise ValidationError(f"Site with id {site_id} does not exist") # Sector ID is REQUIRED if not sector_id: raise ValidationError("sector_id is required. Please select a sector.") try: sector = Sector.objects.get(id=sector_id) if sector.site_id != site_id: raise ValidationError(f"Sector does not belong to the selected site") except Sector.DoesNotExist: raise ValidationError(f"Sector with id {sector_id} does not exist") serializer.validated_data.pop('site_id', None) serializer.validated_data.pop('sector_id', None) account = getattr(self.request, 'account', None) if not account and user and user.is_authenticated and user.account: account = user.account if not account: account = site.account serializer.save(account=account, site=site, sector=sector) @action(detail=False, methods=['post'], url_path='bulk_delete', url_name='bulk_delete') def bulk_delete(self, request): """Bulk delete tasks""" ids = request.data.get('ids', []) if not ids: return Response({'error': 'No IDs provided'}, status=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) @action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update') def bulk_update(self, request): """Bulk update task status""" ids = request.data.get('ids', []) status_value = request.data.get('status') if not ids: return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST) if not status_value: return Response({'error': 'No status provided'}, status=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) @action(detail=False, methods=['post'], url_path='auto_generate_content', url_name='auto_generate_content') def auto_generate_content(self, request): """Auto-generate content for tasks using AI""" import logging from django.db import OperationalError, DatabaseError, IntegrityError from django.core.exceptions import ValidationError logger = logging.getLogger(__name__) try: 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) 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) logger.info(f"auto_generate_content: Processing {len(ids)} task IDs: {ids}") # Get account account = getattr(request, 'account', None) account_id = account.id if account else None logger.info(f"auto_generate_content: Account ID: {account_id}") # Validate task IDs exist in database before proceeding try: queryset = self.get_queryset() existing_tasks = queryset.filter(id__in=ids) existing_count = existing_tasks.count() existing_ids = list(existing_tasks.values_list('id', flat=True)) logger.info(f"auto_generate_content: Found {existing_count} existing tasks out of {len(ids)} requested") logger.info(f"auto_generate_content: Existing task IDs: {existing_ids}") 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) if existing_count < len(ids): missing_ids = set(ids) - set(existing_ids) logger.warning(f"auto_generate_content: Some task IDs not found: {missing_ids}") # Continue with existing tasks, but log warning except (OperationalError, DatabaseError) as db_error: logger.error("=" * 80) logger.error("DATABASE ERROR: Failed to query tasks") logger.error(f" - Error type: {type(db_error).__name__}") logger.error(f" - Error message: {str(db_error)}") logger.error(f" - Requested IDs: {ids}") 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) # Try to queue Celery task, fall back to synchronous if Celery not available try: from .tasks import auto_generate_content_task if hasattr(auto_generate_content_task, 'delay'): # Celery is available - queue async task logger.info(f"auto_generate_content: Queuing Celery task for {len(ids)} tasks") try: task = auto_generate_content_task.delay(ids, 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) except Exception as celery_error: logger.error("=" * 80) logger.error("CELERY ERROR: Failed to queue task") logger.error(f" - Error type: {type(celery_error).__name__}") logger.error(f" - Error message: {str(celery_error)}") logger.error(f" - Task IDs: {ids}") logger.error(f" - Account ID: {account_id}") logger.error("=" * 80, exc_info=True) # Fall back to synchronous execution logger.info("auto_generate_content: Falling back to synchronous execution") result = auto_generate_content_task(ids, account_id=account_id) if result.get('success'): return Response({ 'success': True, 'tasks_updated': result.get('tasks_updated', 0), 'message': 'Content generated successfully (synchronous)' }, status=status.HTTP_200_OK) else: return Response({ 'error': result.get('error', 'Content generation failed'), 'type': 'TaskExecutionError' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: # Celery not available - execute synchronously logger.info(f"auto_generate_content: Executing synchronously (Celery not available)") result = auto_generate_content_task(ids, account_id=account_id) if result.get('success'): logger.info(f"auto_generate_content: Synchronous execution successful: {result.get('tasks_updated', 0)} tasks updated") return Response({ 'success': True, 'tasks_updated': result.get('tasks_updated', 0), 'message': 'Content generated successfully' }, status=status.HTTP_200_OK) 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) except ImportError as import_error: logger.error(f"auto_generate_content: ImportError - tasks module not available: {str(import_error)}") # Tasks module not available - update status only try: queryset = self.get_queryset() tasks = queryset.filter(id__in=ids, status__in=['queued', 'in_progress']) updated_count = tasks.update(status='draft', 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) 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) except (OperationalError, DatabaseError) as db_error: logger.error("=" * 80) logger.error("DATABASE ERROR: Failed during task execution") logger.error(f" - Error type: {type(db_error).__name__}") logger.error(f" - Error message: {str(db_error)}") logger.error(f" - Task IDs: {ids}") 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) except IntegrityError as integrity_error: logger.error("=" * 80) logger.error("INTEGRITY ERROR: Data integrity violation") logger.error(f" - Error message: {str(integrity_error)}") 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) 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) except Exception as e: logger.error("=" * 80) logger.error("UNEXPECTED ERROR in auto_generate_content") logger.error(f" - Error type: {type(e).__name__}") logger.error(f" - Error message: {str(e)}") logger.error(f" - Task IDs: {ids}") 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) except Exception as outer_error: logger.error("=" * 80) logger.error("CRITICAL ERROR: Outer exception handler") logger.error(f" - Error type: {type(outer_error).__name__}") 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) class ImagesViewSet(SiteSectorModelViewSet): """ ViewSet for managing task images """ queryset = Images.objects.all() serializer_class = ImagesSerializer filter_backends = [DjangoFilterBackend, filters.OrderingFilter] ordering_fields = ['created_at', 'position'] ordering = ['task', 'position', '-created_at'] filterset_fields = ['task_id', 'image_type', 'status'] def perform_create(self, serializer): """Override to automatically set account""" account = getattr(self.request, 'account', None) if account: serializer.save(account=account) else: serializer.save() @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) if len(task_ids) > 10: return Response({'error': 'Maximum 10 tasks allowed for image generation'}, status=status.HTTP_400_BAD_REQUEST) # Get account account = getattr(request, 'account', None) account_id = account.id if account else None # Try to queue Celery task, fall back to synchronous if Celery not available try: from .tasks import auto_generate_images_task if hasattr(auto_generate_images_task, 'delay'): # Celery is available - queue async task task = auto_generate_images_task.delay(task_ids, account_id=account_id) return Response({ 'success': True, 'task_id': str(task.id), 'message': 'Image generation started' }, status=status.HTTP_200_OK) else: # Celery not available - execute synchronously result = auto_generate_images_task(task_ids, account_id=account_id) if result.get('success'): return Response({ 'success': True, 'images_created': result.get('images_created', 0), 'message': result.get('message', 'Image generation completed') }, status=status.HTTP_200_OK) else: return Response({ 'error': result.get('error', 'Image generation failed') }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except ImportError: # Tasks module not available return Response({ 'error': 'Image generation task not available' }, status=status.HTTP_503_SERVICE_UNAVAILABLE) class ContentViewSet(SiteSectorModelViewSet): """ ViewSet for managing task content """ queryset = Content.objects.all() serializer_class = ContentSerializer filter_backends = [DjangoFilterBackend, filters.OrderingFilter] ordering_fields = ['generated_at', 'updated_at'] ordering = ['-generated_at'] filterset_fields = ['task_id'] def perform_create(self, serializer): """Override to automatically set account""" account = getattr(self.request, 'account', None) if account: serializer.save(account=account) else: serializer.save()