from rest_framework import viewsets, filters from rest_framework.authentication import TokenAuthentication from django_filters.rest_framework import DjangoFilterBackend from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import IsAuthenticated from api.logger.mixins import LoggingMixin from core.permissions import require_permission, user_has_permission, IsInternalService from mixins.filtrado_organizacion import OrganizacionFiltradaMixin from .models import Task from .serializers import TaskSerializer from .filters import TaskFilter class TaskPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100 class TaskViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Task se relaciona con pedimento, que tiene contribuyente campo_contribuyente = 'pedimento__contribuyente' queryset = Task.objects.select_related('pedimento', 'servicio').all() serializer_class = TaskSerializer filter_backends = [DjangoFilterBackend, filters.OrderingFilter] filterset_class = TaskFilter pagination_class = TaskPagination ordering_fields = ['timestamp'] ordering = ['-timestamp'] my_tags = ['tasks'] def get_permissions(self): # Escritura: exclusivo para microservicio interno (Token + superuser) # Lectura: usuarios con pedimentos.view via JWT if self.action in ('create', 'update', 'partial_update', 'destroy'): return [IsAuthenticated(), IsInternalService()] return [IsAuthenticated(), require_permission('pedimentos.view')()] def get_queryset(self): user = self.request.user # Service account (Token + superuser): sin filtro de org, accede a todas las tasks if user.is_superuser and isinstance( getattr(self.request, 'successful_authenticator', None), TokenAuthentication ): return Task.objects.select_related('pedimento', 'servicio').all() if not user_has_permission(user, 'pedimentos.view'): return Task.objects.none() return self.get_queryset_filtrado_por_organizacion() from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from celery.result import AsyncResult class TaskStatusView(APIView): permission_classes = [IsAuthenticated, require_permission('pedimentos.view')] # Mapeo de status del microservicio → estados estándar _STATUS_MAP = { 'failed': 'FAILURE', 'completed': 'SUCCESS', 'processing': 'STARTED', 'submitted': 'PENDING', 'pending': 'PENDING', } def get(self, request, task_id): """ Consulta el estado de una tarea. Fuente de verdad: registro Django Task (actualizado por el microservicio vía PUT). Celery AsyncResult se usa como complemento para tareas de auditoría masiva (SUCCESS) y como fallback cuando la tarea no está en la BD todavía. Estados posibles: PENDING — en cola o aún no registrada STARTED — worker ejecutando SUCCESS — completada sin errores FAILURE — terminó con error RETRY — el worker la está reintentando """ try: # Prioridad 1: Django Task record (fuente de verdad del microservicio) try: django_task = Task.objects.get(task_id=task_id) effective_state = self._STATUS_MAP.get( django_task.status.lower(), django_task.status.upper() ) is_terminal = effective_state in ('SUCCESS', 'FAILURE') response_data = { 'task_id': task_id, 'status': effective_state, 'ready': is_terminal, 'successful': (effective_state == 'SUCCESS') if is_terminal else None, 'message': django_task.message, } if effective_state == 'FAILURE': response_data['error'] = django_task.message elif effective_state == 'SUCCESS': # Para auditoría masiva, intentar enriquecer con resultado de Celery try: celery_result = AsyncResult(task_id) if celery_result.ready() and celery_result.successful(): result = celery_result.result response_data['result'] = result if isinstance(result, dict) and 'total_pedimentos' in result: total = result.get('total_pedimentos', 0) completados = result.get('completados', 0) con_pendientes = result.get('con_pendientes', 0) con_errores = result.get('con_errores', 0) if con_pendientes == 0 and con_errores == 0: response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes' else: partes = [] if con_pendientes: partes.append(f'{con_pendientes} con documentos pendientes') if con_errores: partes.append(f'{con_errores} con error') response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}' except Exception: pass return Response(response_data, status=status.HTTP_200_OK) except Task.DoesNotExist: pass # Prioridad 2: Celery AsyncResult (tarea aún no registrada en BD) task_result = AsyncResult(task_id) state = task_result.state response_data = { 'task_id': task_id, 'status': state, 'ready': task_result.ready(), 'successful': task_result.successful() if task_result.ready() else None, } if state == 'SUCCESS': result = task_result.result response_data['result'] = result if isinstance(result, dict) and 'total_pedimentos' in result: total = result.get('total_pedimentos', 0) completados = result.get('completados', 0) con_pendientes = result.get('con_pendientes', 0) con_errores = result.get('con_errores', 0) if con_pendientes == 0 and con_errores == 0: response_data['mensaje'] = f'Auditoría completa — {completados}/{total} pedimentos sin pendientes' else: partes = [] if con_pendientes: partes.append(f'{con_pendientes} con documentos pendientes') if con_errores: partes.append(f'{con_errores} con error') response_data['mensaje'] = f'{completados}/{total} pedimentos completos — {", ".join(partes)}' elif state == 'FAILURE': response_data['error'] = str(task_result.info) elif state == 'STARTED': response_data['info'] = str(task_result.info) if task_result.info else None return Response(response_data, status=status.HTTP_200_OK) except Exception as e: return Response( {'error': f'Error al consultar tarea: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR )