import atexit import tempfile from api.utils.storage_service import storage_service from config import settings from rest_framework.pagination import PageNumberPagination from api.customs.models import Pedimento, TipoOperacion, Regimen from django.shortcuts import render from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import action from rest_framework.response import Response from django.http import FileResponse, Http404 import os from .models import DataStage from .serializer import DataStageSerializer from api.logger.mixins import LoggingMixin from core.permissions import get_org_context, is_internal_service_request, require_permission # Create your views here. class DataStagePagination(PageNumberPagination): page_size = 20 # Valor por defecto page_size_query_param = 'page_size' max_page_size = 1000 class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet): """ ViewSet for managing DataStage instances. Provides CRUD operations for DataStage. """ serializer_class = DataStageSerializer model = DataStage my_tags = ['DataStage'] pagination_class = DataStagePagination def get_permissions(self): perms = { 'list': 'datastage.view', 'retrieve': 'datastage.view', 'create': 'datastage.create', 'update': 'datastage.create', 'partial_update': 'datastage.create', 'destroy': 'datastage.delete', 'procesar': 'datastage.process', 'download_datastage': 'datastage.view', 'task_status': 'datastage.view', } codename = perms.get(self.action, 'datastage.view') return [IsAuthenticated(), require_permission(codename)()] def get_queryset(self): if is_internal_service_request(self.request): return DataStage.objects.all().order_by('-created_at') org = get_org_context(self.request.user) if not org: return DataStage.objects.none() return DataStage.objects.filter(organizacion=org).order_by('-created_at') def perform_create(self, serializer): org = get_org_context(self.request.user) datastage = serializer.save(organizacion=org) self._trigger_processing(datastage) def _trigger_processing(self, datastage): from api.datastage.tasks import procesar_datastage_task org = get_org_context(self.request.user) datastage.procesado = True datastage.save() procesar_datastage_task.delay(datastage.id, org.id if org else None) def perform_update(self, serializer): if is_internal_service_request(self.request): serializer.save() return org = get_org_context(self.request.user) serializer.save(organizacion=org) def perform_destroy(self, instance): if instance.archivo: storage_service.delete_file(instance.archivo) instance.delete() @action(detail=True, methods=['get'], url_path='download-datastage', url_name='download-datastage') def download_datastage(self, request, pk=None): """ Endpoint para descargar el archivo asociado a un DataStage. Soporta tanto archivos en MinIO como archivos locales antiguos. """ try: datastage = self.get_object() if not datastage.archivo: raise Http404("No hay archivo asociado a este DataStage.") # Detectar si es ruta de MinIO o local is_minio_path = datastage.archivo.startswith('org_') if is_minio_path: import tempfile with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp_path = tmp.name success = storage_service.download_file(datastage.archivo, tmp_path) if not success: raise Http404("No se pudo descargar el archivo de MinIO") filename = os.path.basename(datastage.archivo) response = FileResponse( open(tmp_path, 'rb'), as_attachment=True, filename=filename ) import atexit atexit.register(lambda: os.unlink(tmp_path) if os.path.exists(tmp_path) else None) return response else: file_path = os.path.join(settings.MEDIA_ROOT, str(datastage.archivo)) if not os.path.exists(file_path): raise Http404(f"El archivo no existe: {file_path}") filename = os.path.basename(file_path) response = FileResponse( open(file_path, 'rb'), as_attachment=True, filename=filename ) return response except Exception as e: return Response({'detail': str(e)}, status=404) def perform_destroy(self, instance): """ Al eliminar un DataStage, también eliminar su archivo asociado. """ if instance.archivo: storage_service.delete_file(instance.archivo) instance.delete() @action(detail=True, methods=['post'], url_path='procesar') def procesar(self, request, pk=None): """ Endpoint para procesar el DataStage de forma asíncrona usando Celery. """ from api.datastage.tasks import procesar_datastage_task datastage = self.get_object() org = get_org_context(self.request.user) task = procesar_datastage_task.delay(datastage.id, org.id if org else None) return Response({ 'task_id': task.id, 'detail': 'Procesamiento iniciado. Puede consultar el estado con el task_id.' }) @action(detail=False, methods=['get'], url_path='task-status') def task_status(self, request): """ Consulta el estado de una tarea de Celery por task_id. """ from celery.result import AsyncResult task_id = request.query_params.get('task_id') if not task_id: return Response({'detail': 'Falta el parámetro task_id.'}, status=400) result = AsyncResult(task_id) return Response({ 'task_id': task_id, 'status': result.status, 'result': result.result if result.successful() else None })