diff --git a/cosiap_api/delete_migrations.py b/cosiap_api/delete_migrations.py new file mode 100644 index 0000000000000000000000000000000000000000..d97a979735258e7ae51fbb91b260d33e82a1868b --- /dev/null +++ b/cosiap_api/delete_migrations.py @@ -0,0 +1,21 @@ +import os +import shutil + +def delete_migrations(project_dir): + for root, dirs, files in os.walk(project_dir): + if 'migrations' in dirs: + migrations_dir = os.path.join(root, 'migrations') + print(f"Eliminando archivos de migración en: {migrations_dir}") + # Borra todos los archivos en el directorio de migraciones, excepto el archivo __init__.py + for filename in os.listdir(migrations_dir): + file_path = os.path.join(migrations_dir, filename) + if filename != '__init__.py' and filename.endswith('.py'): + print(f"Eliminando archivo: {file_path}") + os.remove(file_path) + # Si quieres eliminar también el archivo __init__.py, descomenta la siguiente línea + # shutil.rmtree(migrations_dir) + print(f"Archivos de migración eliminados en: {migrations_dir}") + +if __name__ == "__main__": + project_dir = os.path.dirname(os.path.abspath(__file__)) + delete_migrations(project_dir) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index c9f0e6dadef19e2b753d7d581fd51f5d5cea6705..8c9f73c3e1ed4e0f7cabe03aeef6174f942237b0 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -9,6 +9,7 @@ from .models import DynamicTableReport from django.db import models from collections import defaultdict from django.db.models import ForeignKey, ManyToManyField, OneToOneField, ManyToOneRel, FileField, ImageField +from solicitudes.models import Solicitud import re from django.core.exceptions import ValidationError, FieldDoesNotExist import json @@ -23,7 +24,7 @@ from zipfile import ZipFile import io from dynamic_forms.models import RDocumento -exclude_pattern = re.compile(r'^password$|^last_login$|^created_at$|^updated_at$|^usuario_ptr$|^groups$|^user_permissions$|^dynamic_form$', re.IGNORECASE) +exclude_pattern = re.compile(r'^password$|^last_login$|^created_at$|^updated_at$|^usuario_ptr$|^groups$|^user_permissions$|^dynamic_form__nombre$|^dynamic_form__secciones$', re.IGNORECASE) class DynamicTable(serializers.ModelSerializer): ''' @@ -36,6 +37,12 @@ class DynamicTable(serializers.ModelSerializer): fields = '__all__' + def __init__(self, model_class=None, *args, **kwargs): + if model_class is None: + raise ValueError("El argumento 'model_class' es obligatorio.") + self.model_class = model_class + + def to_representation(self, instance): ''' Método to_representation: Devuelve la representación de la configuración de tabla dinámica excluyendo la data @@ -570,46 +577,72 @@ class DynamicTable(serializers.ModelSerializer): return overall_success - def export_to_csv_and_zip(self, data, uid): + def export_to_csv_and_zip(self, data=None, solicitud_id=None): """ Exporta los datos a un CSV y los archivos asociados a una estructura de directorios, y luego empaqueta todo en un archivo zip. + + Parámetros: + - data: Los datos a exportar. No se espera si se proporciona un solicitud_id. + - uid: Identificador único para el archivo zip. + - solicitud_id: ID de la solicitud específica a exportar. Si se proporciona, se exporta solo esa solicitud. """ # Configuramos los directorios correspondientes - temp_dir = os.path.join(settings.BASE_DIR, 'temp_export', uid) + temp_dir = os.path.join(settings.BASE_DIR, 'temp_export') os.makedirs(temp_dir, exist_ok=True) files_dir = os.path.join(temp_dir, 'archivos') os.makedirs(files_dir, exist_ok=True) csv_file_path = os.path.join(temp_dir, 'reporte.csv') - # Creamos el archivo CSV - with open(csv_file_path, mode='w', newline='', encoding='utf-8') as csvfile: - fieldnames = self.get_fieldnames(data) - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - writer.writeheader() - + if solicitud_id: + solicitud = Solicitud.objects.get(id=solicitud_id) + data_nested = self.retrieve_instance_data(solicitud) + data = self.flatten_dict(data_nested) - solicitudes_por_curp = {} - for item in data: - solicitante_curp = item.get('solicitante__curp') - if solicitante_curp: - solicitud_n = item.get('solicitud_n') - if solicitante_curp not in solicitudes_por_curp: - curp_dir = os.path.join(files_dir, f'solicitud_{solicitante_curp}') - os.makedirs(curp_dir, exist_ok=True) - solicitudes_por_curp[solicitante_curp] = curp_dir - - curp_dir = solicitudes_por_curp[solicitante_curp] - solicitud_dir = os.path.join(curp_dir, f'solicitud_{solicitud_n}') - os.makedirs(solicitud_dir, exist_ok=True) - - respuestas_dir = os.path.join(solicitud_dir, 'respuestas') - os.makedirs(respuestas_dir, exist_ok=True) - self.handle_files(item, solicitud_dir) - self.handle_responses(solicitud_n, respuestas_dir) - else: - self.handle_files(item, files_dir) - writer.writerow(item) + with open(csv_file_path, mode='w', newline='', encoding='utf-8') as csvfile: + fieldnames = self.get_fieldnames(data) + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + solicitante_curp = data.get('solicitante__curp') + id_solicitud = data.get('id') + curp_dir = os.path.join(files_dir, f'solicitud_{solicitante_curp}') + os.makedirs(curp_dir, exist_ok=True) + solicitud_dir = os.path.join(curp_dir, f'solicitud_{id_solicitud}') + os.makedirs(solicitud_dir, exist_ok=True) + + respuestas_dir = os.path.join(solicitud_dir, 'respuestas') + os.makedirs(respuestas_dir, exist_ok=True) + self.handle_files(data, solicitud_dir) + self.handle_responses(id_solicitud, respuestas_dir) + writer.writerow(data) + else: + with open(csv_file_path, mode='w', newline='', encoding='utf-8') as csvfile: + fieldnames = self.get_fieldnames(data) + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + solicitudes_por_curp = {} + + for item in data: + solicitante_curp = item.get('solicitante__curp') + if solicitante_curp: + id_solicitud = item.get('id') + if solicitante_curp not in solicitudes_por_curp: + curp_dir = os.path.join(files_dir, f'solicitud_{solicitante_curp}') + os.makedirs(curp_dir, exist_ok=True) + solicitudes_por_curp[solicitante_curp] = curp_dir + + curp_dir = solicitudes_por_curp[solicitante_curp] + solicitud_dir = os.path.join(curp_dir, f'solicitud_{id_solicitud}') + os.makedirs(solicitud_dir, exist_ok=True) + + respuestas_dir = os.path.join(solicitud_dir, 'respuestas') + os.makedirs(respuestas_dir, exist_ok=True) + self.handle_files(item, solicitud_dir) + self.handle_responses(id_solicitud, respuestas_dir) + else: + self.handle_files(item, files_dir) + writer.writerow(item) # Creamos un archivo zip zip_buffer = io.BytesIO() @@ -627,22 +660,28 @@ class DynamicTable(serializers.ModelSerializer): zip_buffer.seek(0) response = HttpResponse(zip_buffer, content_type='application/zip') - response['Content-Disposition'] = f'attachment; filename="reporte_{uid}.zip"' - - # Limpiamos directorio temporal + response['Content-Disposition'] = f'attachment; filename="reporte.zip"' shutil.rmtree(temp_dir) - return response + def get_fieldnames(self, data): """ Devuelve los nombres de los campos que se usarán en el CSV. + Maneja tanto listas de diccionarios como un solo diccionario. """ if not data: return [] - first_item = data[0] + # Verifica si 'data' es una lista de diccionarios + if isinstance(data, list): + first_item = data[0] + elif isinstance(data, dict): + first_item = data + else: + raise ValueError("El formato de los datos no es compatible") + return list(first_item.keys()) @@ -651,9 +690,14 @@ class DynamicTable(serializers.ModelSerializer): Maneja los archivos adjuntos, los guarda en el directorio correspondiente basado en el nombre de la columna. """ for key, value in item.items(): + print(key, '-', value) if value and isinstance(value, str) and value.endswith(('.png', '.jpg', '.jpeg', '.pdf')): # Detectar directorio basado en la primera parte del nombre de la columna column_dir = key.split('__')[0] + + if value.startswith('/media/'): + value = value[len('/media/'):] + if column_dir == 'modalidad': continue @@ -676,12 +720,12 @@ class DynamicTable(serializers.ModelSerializer): continue - def handle_responses(self, solicitud_n, respuestas_dir): + def handle_responses(self, id_solicitud, respuestas_dir): """ Maneja la extracción de respuestas relacionadas con la solicitud y las guarda en el directorio correspondiente. """ # Filtrar las respuestas por solicitud - responses = RDocumento.objects.filter(registro_id=solicitud_n) + responses = RDocumento.objects.filter(registro_seccion_id=id_solicitud) for response in responses: file_path = os.path.join(respuestas_dir, os.path.basename(response.valor.name)) @@ -694,3 +738,20 @@ class DynamicTable(serializers.ModelSerializer): response.archivo = os.path.join('archivos', 'respuestas', os.path.basename(response.valor.name)) response.save() + + def flatten_dict(self,d, parent_key='', sep='__'): + ''' + Método para convertir un diccionario anidado a un dichionario plano + + param:d: diccionario anidado a convertir + param:parent_key: parámetro para marcar los keys padres durante el ciclo + param:sep: separador de columna_relacion + ''' + items = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(self.flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py index 94503d7f890bbb3fa5616532e09787f8419cf79e..73a3c690444d81313acc5abd8bff849d11013742 100644 --- a/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py +++ b/cosiap_api/dynamic_tables/DynamicTableDynamicForm.py @@ -1,10 +1,53 @@ from .DynamicTable import DynamicTable +from dynamic_forms.models import DynamicForm +from solicitudes.models import Solicitud +from dynamic_forms.serializers import OpcionSerializer, ElementosOpcionesSerializer, ElementoSerializer, SeccionesElementosSerializer, SeccionSerializer, DynamicFormsSeccionesSerializer, DynamicFormSerializer, RespuestaFormularioSerializer + class DynamicTableDynamicForm(DynamicTable): - - def get_dynamicform_fields(self): - ''' Aquí se realizará la lógica para la obtención de las columnas del dynamic_form''' - pass + ''' + Clase que hereda de DynamicTable con el objetivo de especializar su comportamiento para la extracción de los + datos de un formulario dinámico + ''' + + + def get_data(self, obj): + ''' + Método sobreescrito para obtener los datos tanto de la solicitud como de el formulario + dinámico asociado a ella. + ''' + solicitudes = super().get_data(obj) + solicitud_ids = [solicitud.get('id') for solicitud in solicitudes if solicitud.get('id')] + solicitud_instances = Solicitud.objects.filter(id__in=solicitud_ids).select_related('modalidad__dynamic_form') + + # Serializar las solicitudes utilizando RespuestaFormularioSerializer, pasando dynamic_form_source como parámetro + serialized_forms = RespuestaFormularioSerializer(solicitud_instances, many=True, dynamic_form_source='modalidad__dynamic_form') + + forms_data = serialized_forms.data + forms_data_dict = {form['id']: form for form in forms_data} + + for solicitud in solicitudes: + dynamic_form_id = solicitud.get('modalidad__dynamic_form__id') + if dynamic_form_id: + solicitud['formulario'] = forms_data_dict.get(dynamic_form_id) + + return solicitudes + + def retrieve_instance_data(self, instance): + ''' + Método sobreescrito para la vista detallada de la solicitud incluyendo su formulario dinámico + ''' + solicitud = super().retrieve_instance_data(instance) + solicitud_id = solicitud.get('id') + solicitud_instance = Solicitud.objects.get(id=solicitud_id) + + form = RespuestaFormularioSerializer(solicitud_instance, dynamic_form_source='modalidad__dynamic_form') + form_data = form.data + + solicitud['formulario'] = form_data + + return solicitud + def get_dynamicform_filters(self): ''' Aquí se realizará la lógica para la obtención de los filtros disponibles de dynamic_form''' diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index e994cc2cec746efa904125086df79aa2fb9c184f..0079336b3e8cecb953222ed90848d8b781c915fb 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render, get_object_or_404 from rest_framework.response import Response from rest_framework import status from dynamic_tables.DynamicTable import DynamicTable +from dynamic_tables.DynamicTableDynamicForm import DynamicTableDynamicForm from rest_framework.permissions import AllowAny, IsAuthenticated from dynamic_tables.models import DynamicTableReport from users.views import BasePermissionAPIView @@ -12,6 +13,7 @@ from django.core.exceptions import ValidationError import json from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from solicitudes.models import Solicitud class DynamicTableAPIView(BasePermissionAPIView): @@ -20,7 +22,7 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' permission_classes_update = [IsAuthenticated, es_admin] - permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_list = [IsAuthenticated, es_admin] permission_classes_create = [IsAuthenticated, es_admin] permission_classes_delete = [IsAuthenticated, es_admin] @@ -34,6 +36,8 @@ class DynamicTableAPIView(BasePermissionAPIView): # Lista con los campos que no pueden ser modificados mediante la solicitud put non_editable_fields = [] + dynamic_form_exist = False + def get_configuracion_reporte(self, request): ''' Crear una configuración con los datos actuales de la request, sobrescribiendo los predeterminados si se proporcionan. @@ -89,15 +93,21 @@ class DynamicTableAPIView(BasePermissionAPIView): if pk is not None: # Recuperar la instancia y extraer los datos instance = get_object_or_404(self.model_class, pk=pk) - serializer = DynamicTable() + if self.dynamic_form_exist: + serializer = DynamicTableDynamicForm(model_class=self.model_class) + else: + serializer = DynamicTable() instance_data = serializer.retrieve_instance_data(instance) return Response(instance_data, status=status.HTTP_200_OK) try: configuracion_reporte = self.get_configuracion_reporte(request) - serializer = DynamicTable(instance=configuracion_reporte) + if self.dynamic_form_exist: + serializer = DynamicTableDynamicForm(instance=configuracion_reporte, model_class=self.model_class) + else: + serializer = DynamicTable(instance=configuracion_reporte,model_class=self.model_class) data = serializer.get_data(configuracion_reporte) - available_filters = serializer.get_available_filters(configuracion_reporte) available_columns = serializer.get_available_columns(configuracion_reporte) + available_filters = serializer.get_available_filters(configuracion_reporte) response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} return Response(response_data, status=status.HTTP_200_OK) except Exception as e: @@ -122,7 +132,10 @@ class DynamicTableAPIView(BasePermissionAPIView): # Extraer la configuración, o si no fue enviada asignamos la predeterminada configuracion = self.get_configuracion_reporte(request) - serializer = DynamicTable(instance=configuracion) + if self.dynamic_form_exist: + serializer = DynamicTableDynamicForm(instance=configuracion,model_class=self.model_class) + else: + serializer = DynamicTable(instance=configuracion,model_class=self.model_class) if self.columns == "__all__": self.columns = list(serializer.get_available_columns(configuracion).keys()) @@ -259,7 +272,7 @@ class Exportar_CSV(BasePermissionAPIView): APIView para manejar la exportación de los datos según una configuración de reporte. ''' permission_classes_list = [IsAuthenticated, es_admin] - model_name = None + model_class = None def get_configuracion_reporte(self, request): ''' @@ -283,12 +296,15 @@ class Exportar_CSV(BasePermissionAPIView): Método get para obtener el archivo zip con todos los documentos solicitados. ''' - configuracion = self.get_configuracion_reporte(request) - reporte = DynamicTable(instance=configuracion) - data = reporte.get_data(configuracion) - # creamos un UID encriptado para la creación del ZIP único del usuario. - uid = urlsafe_base64_encode(force_bytes(request.user.pk)) - - # exportamos los datos - response = reporte.export_to_csv_and_zip(data, uid) - return response + if 'pk' in kwargs: + reporte = DynamicTableDynamicForm(model_class=self.model_class) + solicitud_id = kwargs['pk'] + response = reporte.export_to_csv_and_zip(solicitud_id=solicitud_id) + return response + else: + configuracion = self.get_configuracion_reporte(request) + reporte = DynamicTableDynamicForm(instance=configuracion, model_class=self.model_class) + data = reporte.get_data(configuracion) + + response = reporte.export_to_csv_and_zip(data) + return response \ No newline at end of file diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index c69c3a82b6545dd7895bc179c81de5dfe5f4d569..adeb962d8f6f27fc570aefbe7b42e7b2dc661d04 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -1,7 +1,6 @@ from . import views from django.urls import path from django.contrib.auth import views as auth_views -from dynamic_tables.views import Exportar_CSV app_name = 'solicitudes' @@ -12,5 +11,6 @@ urlpatterns = [ path('historial//', views.HistorialAPIVIew.as_view(), name='historial_pk'), path('reportes/', views.ReportesSolicitudesAPIView.as_view(), name='reportes_solicitudes'), path('reportes//', views.ReportesSolicitudesAPIView.as_view(), name='reportes_solicitudes_pk'), - path('reportes/exportar/', Exportar_CSV.as_view(), name='exportar_reportes'), + path('reportes/exportar/', views.ExportarReporteSolicitudes.as_view(), name='exportar_reportes'), + path('reportes/exportar//', views.ExportarReporteSolicitudes.as_view(), name='exportar_reportes_pk'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 6cecdef214efc2e106e9ebce71135f691ae53d73..58193421a2c9f69f15ddd0abec1d02c51a15c086 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -19,6 +19,7 @@ from dynamic_tables.views import ReporteAPIView from rest_framework.permissions import AllowAny from dynamic_tables.models import DynamicTableReport from dynamic_tables.DynamicTable import DynamicTable +from dynamic_tables.views import Exportar_CSV class SolicitudAPIView(DynamicTableAPIView): ''' @@ -36,6 +37,7 @@ class SolicitudAPIView(DynamicTableAPIView): } } non_editable_fields = ["status"] + dynamic_form_exist = True class HistorialAPIVIew(BasePermissionAPIView): @@ -94,3 +96,10 @@ class ReportesSolicitudesAPIView(ReporteAPIView): return Response(serializer.data) +class ExportarReporteSolicitudes(Exportar_CSV): + ''' + Clase que hereda de Exportar_CSV de tablas dinámicas + especializando la exportación para solicitudes. + ''' + model_class = Solicitud +