import logging from rest_framework import status from rest_framework.decorators import action from rest_framework.viewsets import ViewSet from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.response import success_response, error_response from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.business.content.models import Content from igny8_core.business.linking.services.linker_service import LinkerService from igny8_core.business.billing.exceptions import InsufficientCreditsError from igny8_core.modules.linker.serializers import ( LinkContentSerializer, BatchLinkContentSerializer, ) logger = logging.getLogger(__name__) @extend_schema_view( process=extend_schema(tags=['Linker']), batch_process=extend_schema(tags=['Linker']), ) class LinkerViewSet(ViewSet): """ API endpoints for internal linking operations. Unified API Standard v1.0 compliant """ permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove] throttle_scope = 'linker' throttle_classes = [DebugScopedRateThrottle] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.linker_service = LinkerService() @action(detail=False, methods=['post']) def process(self, request): """ Process a single content item for internal linking. POST /api/v1/linker/process/ { "content_id": 123 } """ serializer = LinkContentSerializer(data=request.data, context={'request': request}) if not serializer.is_valid(): return error_response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, request=request) content_id = serializer.validated_data['content_id'] try: content = self.linker_service.process(content_id) result = { 'content_id': content.id, 'links_added': len(content.internal_links) if content.internal_links else 0, 'links': content.internal_links or [], 'linker_version': content.linker_version, 'success': True, } return success_response(result, request=request) except ValueError as e: return error_response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST, request=request) except InsufficientCreditsError as e: return error_response({'error': str(e)}, status=status.HTTP_402_PAYMENT_REQUIRED, request=request) except Exception as e: logger.error(f"Error processing content {content_id}: {str(e)}", exc_info=True) return error_response({'error': 'Internal server error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request) @action(detail=False, methods=['post']) def batch_process(self, request): """ Process multiple content items for internal linking. POST /api/v1/linker/batch_process/ { "content_ids": [123, 456, 789] } """ serializer = BatchLinkContentSerializer(data=request.data, context={'request': request}) if not serializer.is_valid(): return error_response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, request=request) content_ids = serializer.validated_data['content_ids'] try: results = self.linker_service.batch_process(content_ids) response_data = [] for content in results: response_data.append({ 'content_id': content.id, 'links_added': len(content.internal_links) if content.internal_links else 0, 'links': content.internal_links or [], 'linker_version': content.linker_version, 'success': True, }) return success_response(response_data, request=request) except Exception as e: logger.error(f"Error batch processing content: {str(e)}", exc_info=True) return error_response({'error': 'Internal server error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request)