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, models from django.db.models import Q from drf_spectacular.utils import extend_schema, extend_schema_view 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 igny8_core.api.permissions import IsAuthenticatedAndActive, IsViewerOrAbove, IsEditorOrAbove from .models import Tasks, Images, Content from .serializers import ( TasksSerializer, ImagesSerializer, ContentSerializer, ContentTaxonomySerializer, ) from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute model exists but serializer removed in Stage 1 from igny8_core.business.content.services.content_generation_service import ContentGenerationService from igny8_core.business.content.services.validation_service import ContentValidationService from igny8_core.business.content.services.metadata_mapping_service import MetadataMappingService from igny8_core.business.billing.exceptions import InsufficientCreditsError @extend_schema_view( list=extend_schema(tags=['Writer']), create=extend_schema(tags=['Writer']), retrieve=extend_schema(tags=['Writer']), update=extend_schema(tags=['Writer']), partial_update=extend_schema(tags=['Writer']), destroy=extend_schema(tags=['Writer']), ) class TasksViewSet(SiteSectorModelViewSet): """ ViewSet for managing tasks with CRUD operations Unified API Standard v1.0 compliant Stage 1 Refactored - removed deprecated filters """ queryset = Tasks.objects.select_related('cluster', 'site', 'sector') serializer_class = TasksSerializer permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove] pagination_class = CustomPageNumberPagination # Explicitly use custom pagination throttle_scope = 'writer' throttle_classes = [DebugScopedRateThrottle] # DRF filtering configuration filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] # Search configuration search_fields = ['title', 'keywords'] # Ordering configuration ordering_fields = ['title', 'created_at', 'status'] ordering = ['-created_at'] # Default ordering (newest first) # Filter configuration - Stage 1: removed entity_type, cluster_role 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 with improved error handling""" ids = request.data.get('ids', []) if not ids: return error_response( error='No IDs provided', status_code=status.HTTP_400_BAD_REQUEST, request=request ) # Validate that all IDs are integers try: ids = [int(id) for id in ids] except (ValueError, TypeError): return error_response( error='Invalid ID format. All IDs must be integers.', status_code=status.HTTP_400_BAD_REQUEST, request=request ) # Get queryset with proper filtering queryset = self.get_queryset() # Filter by IDs and account/site context filtered_queryset = queryset.filter(id__in=ids) # Check if user has permission to delete all tasks if not filtered_queryset.exists(): return error_response( error='No valid tasks found for deletion', status_code=status.HTTP_404_NOT_FOUND, request=request ) # Perform bulk delete try: deleted_count, _ = filtered_queryset.delete() # Return success response return success_response( data={'deleted_count': deleted_count}, message=f'Successfully deleted {deleted_count} task(s)', request=request ) except Exception as e: return error_response( error=f'Failed to delete tasks: {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): """Bulk update task status""" ids = request.data.get('ids', []) status_value = request.data.get('status') if not ids: return error_response( error='No IDs provided', status_code=status.HTTP_400_BAD_REQUEST, request=request ) if not status_value: 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 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): """Auto-generate content for tasks using ContentGenerationService""" import logging logger = logging.getLogger(__name__) try: ids = request.data.get('ids', []) if not ids: return error_response( error='No IDs provided', status_code=status.HTTP_400_BAD_REQUEST, request=request ) if len(ids) > 10: return error_response( error='Maximum 10 tasks allowed for content generation', status_code=status.HTTP_400_BAD_REQUEST, request=request ) # Get account account = getattr(request, 'account', None) if not account: return error_response( error='Account is required', status_code=status.HTTP_400_BAD_REQUEST, request=request ) # Validate task IDs exist queryset = self.get_queryset() existing_tasks = queryset.filter(id__in=ids, account=account) existing_count = existing_tasks.count() if existing_count == 0: return error_response( error=f'No tasks found for the provided IDs: {ids}', status_code=status.HTTP_404_NOT_FOUND, request=request ) # Use service to generate content service = ContentGenerationService() try: result = service.generate_content(ids, account) if result.get('success'): if 'task_id' in result: # Async task queued return success_response( data={'task_id': result['task_id']}, message=result.get('message', 'Content generation started'), request=request ) else: # Synchronous execution return success_response( data=result, message='Content generated successfully', request=request ) else: return error_response( error=result.get('error', 'Content generation failed'), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request ) except InsufficientCreditsError as e: return error_response( error=str(e), status_code=status.HTTP_402_PAYMENT_REQUIRED, request=request ) except Exception as e: logger.error(f"Error in auto_generate_content: {str(e)}", exc_info=True) return error_response( error=f'Content generation failed: {str(e)}', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request ) except Exception as e: logger.error(f"Unexpected error in auto_generate_content: {str(e)}", exc_info=True) return error_response( error=f'Unexpected error: {str(e)}', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request )