276 lines
11 KiB
Python
276 lines
11 KiB
Python
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
|
|
)
|