From 0af385898e935fc9b983da1b3cf7d2978d2233a0 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 11 Jul 2024 12:39:07 -0600 Subject: [PATCH 1/8] =?UTF-8?q?parte=201=20de=20tabla=20din=C3=A1mica=20de?= =?UTF-8?q?=20solicitudes=20y=20aplicaci=C3=B3n=20de=20filtros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/common/views.py | 2 +- cosiap_api/dynamic_tables/DynamicTable.py | 124 +++++++++++------- cosiap_api/dynamic_tables/models.py | 5 - .../migrations/0003_crear_tabla_solicitud.py | 66 ++++++++++ cosiap_api/solicitudes/urls.py | 4 +- cosiap_api/solicitudes/views.py | 90 ++++++++++++- 6 files changed, 236 insertions(+), 55 deletions(-) create mode 100644 cosiap_api/solicitudes/migrations/0003_crear_tabla_solicitud.py diff --git a/cosiap_api/common/views.py b/cosiap_api/common/views.py index 91ea44a..b588925 100644 --- a/cosiap_api/common/views.py +++ b/cosiap_api/common/views.py @@ -1,3 +1,3 @@ from django.shortcuts import render -# Create your views here. + diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 250b935..a40f797 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -25,51 +25,74 @@ class DynamicTable(serializers.ModelSerializer): - obj: Configuración de reporte recibida, la cuál indica que datos se van a recuperar ''' - # Primero tenemos que buscar el modelo en las aplicaciones registradas en el sistema - # Para asegurarnos de que el modelo sea único y sea el deseado. - for app_config in apps.get_app_configs(): - try: - model = app_config.get_model(obj.model_name) - break - except LookupError: - continue - else: - # Si no encontramos el modelo enviamos un error - raise serializers.ValidationError(f"Model {obj.model_name} not found.") - - columns = obj.columns - exclude_columns = obj.exclude_columns or [] # Si es nulo, convertimos a una lista vacía para el mejor manejo - search_query = obj.search_query - filters = obj.filters or {} # si es nulo, convertimos a dict vacío para mejor manejo - - queryset = model.objects.all() - - # En este for, aplicamos los filtros envíados sobre el queryset + try: + # Primero tenemos que buscar el modelo en las aplicaciones registradas en el sistema + # Para asegurarnos de que el modelo sea único y sea el deseado. + for app_config in apps.get_app_configs(): + try: + model = app_config.get_model(obj.model_name) + break + except LookupError: + continue + else: + # Si no encontramos el modelo enviamos un error + raise serializers.ValidationError(f"Model {obj.model_name} not found.") + + columns = obj.columns + exclude_columns = obj.exclude_columns or [] # Si es nulo, convertimos a una lista vacía para el mejor manejo + search_query = obj.search_query + filters = obj.filters or {} # Si es nulo, convertimos a dict vacío para mejor manejo + + queryset = model.objects.all() + + # Aplicamos los filtros usando la función recursiva + queryset = self.apply_filters(queryset, filters) + + # Aplicamos el searchquery enviado y extraemos la información de los campos + if search_query: + search_fields = [field for field in model._meta.fields if field.name in columns] + search_criteria = Q() + for field in search_fields: + search_criteria |= Q(**{f"{field.name}__icontains": search_query}) + queryset = queryset.filter(search_criteria) + + # Seleccionamos las columnas a incluir y las columnas a excluir + queryset = queryset.values(*[col for col in columns if col not in exclude_columns]) + + # Finalmente incluimos todos los campos que estén relacionados a los modelos por las llaves foráneas + for field in model._meta.get_fields(): + if (field.is_relation and + (field.name not in exclude_columns) and + (field.related_model is not None)): + related_queryset = field.related_model.objects.all() + queryset = queryset.prefetch_related(Prefetch(field.name, queryset=related_queryset)) + + # Devolvemos el nuevo queryset con filtros y exclusiones realizadas + return list(queryset) + except Exception as e: + # Si hay algun fallo dentro de la lógica de la extracción de datos, regresamos una lista vacía + return [] + + @staticmethod + def apply_filters(queryset, filters): + """ + Aplica filtros recursivamente a un queryset basado en un diccionario de filtros. + + Funciona extrayendo los keys y los values del diccionario de filtros enviado como parámetro. + + """ + if not filters: + return queryset + + # Extraemos key (la búsqueda en base de datos) y value (el valor que debe tener) for key, value in filters.items(): - queryset = queryset.filter(**{key: value}) - - # Aplicamos el searchquery enviado y extraemos la información de los campos - if search_query: - search_fields = [field for field in model._meta.fields if field.name in columns] - search_criteria = Q() - for field in search_fields: - search_criteria |= Q(**{f"{field.name}__icontains": search_query}) - queryset = queryset.filter(search_criteria) - - # Seleccionamos las columnas a incluir y las columnas a excluir - queryset = queryset.values(*[col for col in columns if col not in exclude_columns]) - - # Finalmente incluimos todos los campos que estén relacionados a los modelos por las llaves foráneas - for field in model._meta.get_fields(): - if (field.is_relation and - (field.name not in exclude_columns) and - (field.related_model is not None)): - related_queryset = field.related_model.objects.all() - queryset = queryset.prefetch_related(Prefetch(field.name, queryset=related_queryset)) - - # devolvemos el nuevo queryset con filtros y exclusiones realizadas - return list(queryset) - + if isinstance(value, dict): + nested_queryset = queryset.filter(**{f"{key}__isnull": False}) + nested_queryset = DynamicTable.apply_filters(nested_queryset, value) + queryset = queryset.filter(id__in=nested_queryset.values('id')) + else: + queryset = queryset.filter(**{key: value}) + return queryset def create(self, validated_data): ''' @@ -79,12 +102,19 @@ class DynamicTable(serializers.ModelSerializer): - validated_data: Datos validados según la configuración de la base de datos ''' - # Aquí usamos el get_or_create para asegurarnos de no guardar dos configuraciones que sean exactamente iguales - instance, created = DynamicTableReport.objects.get_or_create( + # Verificamos si ya existe una configuración igual en la base de datos + instance = DynamicTableReport.objects.filter( model_name=validated_data['model_name'], columns=validated_data['columns'], exclude_columns=validated_data.get('exclude_columns', None), search_query=validated_data.get('search_query', None), filters=validated_data.get('filters', None) - ) + ).first() + + if instance: + # Si ya existe una configuración igual, la retornamos + return instance + + # Si no existe, creamos una nueva instancia + instance = DynamicTableReport.objects.create(**validated_data) return instance diff --git a/cosiap_api/dynamic_tables/models.py b/cosiap_api/dynamic_tables/models.py index cbd4ac8..e750574 100644 --- a/cosiap_api/dynamic_tables/models.py +++ b/cosiap_api/dynamic_tables/models.py @@ -16,11 +16,6 @@ class DynamicTableReport(models.Model): exclude_columns = models.JSONField(blank=True, null=True) search_query = models.CharField(max_length=100, blank=True, null=True) filters = models.JSONField(blank=True, null=True) - - # Incluimos la logica de unique para no guardar configuraciones que sean exactamente iguales - class Meta: - unique_together = ('model_name', 'columns', 'exclude_columns', 'search_query', 'filters') - def __str__(self): return 'Tabla Dinámica: ' + self.model_name diff --git a/cosiap_api/solicitudes/migrations/0003_crear_tabla_solicitud.py b/cosiap_api/solicitudes/migrations/0003_crear_tabla_solicitud.py new file mode 100644 index 0000000..7af77e8 --- /dev/null +++ b/cosiap_api/solicitudes/migrations/0003_crear_tabla_solicitud.py @@ -0,0 +1,66 @@ +# Generated by Django 5.0.6 on 2024-07-11 15:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('modalidades', '0001_creacion_inicial_modulos_dynamic_formats__modalidades__solicitudes'), + ('solicitudes', '0002_creacion_modelo_solicitud'), + ('users', '0010_creacion_modulos_admin_formats_forms_tables_modalidades_solicitudes'), + ] + + operations = [ + migrations.AlterField( + model_name='solicitud', + name='convenio', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='solicitudes.convenio', verbose_name='Convenio'), + ), + migrations.AlterField( + model_name='solicitud', + name='minuta', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='solicitudes.minuta', verbose_name='Minuta'), + ), + migrations.AlterField( + model_name='solicitud', + name='modalidad', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='modalidades.modalidad', verbose_name='Modalidad'), + ), + migrations.AlterField( + model_name='solicitud', + name='monto_aprobado', + field=models.FloatField(default=0.0, verbose_name='Monto Aprobado'), + ), + migrations.AlterField( + model_name='solicitud', + name='monto_solicitado', + field=models.FloatField(default=0.0, verbose_name='Monto Solicitado'), + ), + migrations.AlterField( + model_name='solicitud', + name='observacion', + field=models.TextField(blank=True, null=True, verbose_name='Observación'), + ), + migrations.AlterField( + model_name='solicitud', + name='solicitante', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.solicitante', verbose_name='Solicitante'), + ), + migrations.AlterField( + model_name='solicitud', + name='solicitud_n', + field=models.AutoField(primary_key=True, serialize=False, verbose_name='Num. Solicitud'), + ), + migrations.AlterField( + model_name='solicitud', + name='status', + field=models.CharField(max_length=255, verbose_name='Status'), + ), + migrations.AlterField( + model_name='solicitud', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, verbose_name='Timestamp'), + ), + ] diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index 4a1b414..2e64de1 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -4,4 +4,6 @@ from django.contrib.auth import views as auth_views app_name = 'solicitudes' -urlpatterns = [] \ No newline at end of file +urlpatterns = [ + path('', views.SolicitudAPIView.as_view(), name='solicitud-list'), + ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 91ea44a..292466f 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -1,3 +1,91 @@ +# Archivo con la vista para manejar las solicitudes +# Autores: Adalberto Cerrillo Vázquez +# Versión: 1.0 + from django.shortcuts import render +from users.views import BasePermissionAPIView +from rest_framework.response import Response +from rest_framework import status +from dynamic_tables.DynamicTable import DynamicTable +from dynamic_tables.models import DynamicTableReport +from .models import Solicitud +from users.permisos import es_admin +from django.utils import timezone +from datetime import timedelta + + +class SolicitudAPIView(BasePermissionAPIView): + ''' + Clase para el manejo de la lista de solicitudes y la aplicación de sus filtros + + herencia: BasePermissionAPIView, clase con los permisos predeterminados para los usuarios + ''' + + permission_classes_create = [es_admin] + + def get(self, request): + ''' + En la solicitud get manejamos la lógica para la lista de solicitudes mediante la tabla dinámica + ''' + # necesitamos unicamente las solicitudes realizadas hace 5 meses + fecha_limite = timezone.now() - timedelta(days=5 * 30) # 5 meses estimados en días + # Convertimos fecha_limite a una cadena formateada + fecha_limite_str = fecha_limite.strftime('%Y-%m-%d %H:%M:%S') + # Si aún no se ha creado el reporte por defecto (con toda la información), lo creamos + configuracion_reporte, created = DynamicTableReport.objects.get_or_create( + model_name='Solicitud', + columns=[ + 'status', 'solicitud_n', 'minuta__archivo', 'convenio__archivo', 'monto_solicitado', 'monto_aprobado', + 'modalidad__nombre', 'modalidad__imagen', 'modalidad__descripcion', 'timestamp', 'observacion', + 'solicitante__nombre', 'solicitante__ap_paterno', 'solicitante__ap_materno', 'solicitante__telefono', + 'solicitante__RFC', 'solicitante__direccion', 'solicitante__codigo_postal', 'solicitante__municipio__nombre', + 'solicitante__municipio__estado__nombre', 'solicitante__poblacion', 'solicitante__INE', 'solicitante__datos_bancarios__nombre_banco', + 'solicitante__datos_bancarios__cuenta_bancaria', 'solicitante__datos_bancarios__clabe_bancaria', + 'solicitante__datos_bancarios__codigo_postal_fiscal', 'solicitante__datos_bancarios__regimen' + ], + exclude_columns=[], + search_query=None, + filters={'timestamp__gte':fecha_limite_str} + ) + + # Inicializamos un objeto del tipo dynamicTable para obtener la lista + serializer = DynamicTable(instance=configuracion_reporte) + + # Obtenemos los datos usando el método get_data de nuestro serializer + data = serializer.get_data(configuracion_reporte) + + # Devolvemos la respuesta + return Response(data, status=status.HTTP_200_OK) + + + def post(self, request, *args, **kwargs): + ''' + En la solicitud post manejamos el caso del envio de filtros, exclusion de columnas y searchquerys + + + Ejemplo para envío JSON con filtros: (Para comprender mejor como vienen los datos en el request) + + - Obtención de todas las solicitudes cuyo monto aprobado sea mayor a 10000: + + { + "model_name": "Solicitud", + "columns": ["status", "solicitud_n", "minuta", "convenio", "monto_solicitado", "monto_aprobado", "modalidad", "timestamp", "observacion", "solicitante"], + "exclude_columns": [], + "search_query": "", + "filters": { + "monto_aprobado__gt": 10000 + } + } + ''' + # Debemos inicializar el serializer con la request enviada + serializer = DynamicTable(data=request.data) + if serializer.is_valid(): + # Guarda la configuración si no existe ya + report = serializer.save() + + # Recupera los datos según la configuración + data = serializer.get_data(report) -# Create your views here. + return Response(data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -- GitLab From 204ce16f54d1749b0312324f8eacb43bbf021203 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Fri, 12 Jul 2024 13:04:11 -0600 Subject: [PATCH 2/8] =?UTF-8?q?A=C3=B1adida=20l=C3=B3gica=20de=20filtros?= =?UTF-8?q?=20disponibles=20y=20uso=20de=20operadores=20para=20la=20aplica?= =?UTF-8?q?ci=C3=B3n=20de=20filtros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 90 ++++++++++++++++++++--- cosiap_api/solicitudes/views.py | 27 +++++-- cosiap_api/users/models.py | 27 +++---- 3 files changed, 114 insertions(+), 30 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index a40f797..7eed6b3 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -6,6 +6,7 @@ from rest_framework import serializers from django.apps import apps from django.db.models import Q, Prefetch from .models import DynamicTableReport +from django.db import models class DynamicTable(serializers.ModelSerializer): ''' @@ -17,12 +18,13 @@ class DynamicTable(serializers.ModelSerializer): model = DynamicTableReport fields = '__all__' - def get_data(self, obj): + def get_data(self, obj, logical_operator): ''' Método que se encargará de recuperar los datos correspondientes del modelo enviado parámetros: - obj: Configuración de reporte recibida, la cuál indica que datos se van a recuperar + - logical_operator: operador lógico para la aplicación de los filtros ''' try: @@ -46,7 +48,7 @@ class DynamicTable(serializers.ModelSerializer): queryset = model.objects.all() # Aplicamos los filtros usando la función recursiva - queryset = self.apply_filters(queryset, filters) + queryset = self.apply_filters(queryset, filters, logical_operator) # Aplicamos el searchquery enviado y extraemos la información de los campos if search_query: @@ -73,26 +75,94 @@ class DynamicTable(serializers.ModelSerializer): # Si hay algun fallo dentro de la lógica de la extracción de datos, regresamos una lista vacía return [] + + def get_available_filters(self, obj): + ''' + Método para obtener los filtros disponibles a aplicar a una configuración de tabla dinámica + con la finalidad de que dichos filtros puedan ser seleccionados desde el frontend + + parámetros: + - obj: objeto de con la configuración actual de la tabla dinámica + ''' + available_filters = {} + + # Buscamos el modelo en las aplicaciones registradas + for app_config in apps.get_app_configs(): + try: + model = app_config.get_model(obj.model_name) + break + except LookupError: + continue + else: + raise serializers.ValidationError(f"Model {obj.model_name} not found.") + + for column in obj.columns: + field_path = column.split('__') + field = model + + for part in field_path: + field = field._meta.get_field(part).related_model if field._meta.get_field(part).is_relation else field._meta.get_field(part) + + if isinstance(field, models.CharField) or isinstance(field, models.TextField): + if field.choices: + available_filters[column] = [choice[0] for choice in field.choices] + else: + available_filters[column] = ['icontains'] + elif isinstance(field, models.IntegerField) or isinstance(field, models.FloatField) or isinstance(field, models.DecimalField): + available_filters[column] = ['gt', 'lt'] + elif isinstance(field, models.DateField) or isinstance(field, models.DateTimeField): + available_filters[column] = ['date_min', 'date_max'] + + return available_filters + + @staticmethod - def apply_filters(queryset, filters): + def apply_filters(queryset, filters, logical_operator): """ - Aplica filtros recursivamente a un queryset basado en un diccionario de filtros. + Aplica filtros recursivamente a un queryset basado en un diccionario de filtros + y un operador lógico. - Funciona extrayendo los keys y los values del diccionario de filtros enviado como parámetro. + :param queryset: Queryset original al que se le aplican los filtros. + :param filters: Diccionario de filtros donde las claves son los campos y los valores son los criterios de filtro. + :param logical_operator: Operador lógico a aplicar ('AND', 'OR', 'NOT') + :return: Queryset filtrado según los filtros y el operador lógico especificado. """ if not filters: return queryset - # Extraemos key (la búsqueda en base de datos) y value (el valor que debe tener) + q_objects = Q() + initial = True # Flag para el primer filtro + for key, value in filters.items(): if isinstance(value, dict): nested_queryset = queryset.filter(**{f"{key}__isnull": False}) - nested_queryset = DynamicTable.apply_filters(nested_queryset, value) - queryset = queryset.filter(id__in=nested_queryset.values('id')) + nested_queryset = DynamicTable.apply_filters(nested_queryset, value, logical_operator) + if initial: + q_objects = Q(id__in=nested_queryset.values('id')) + initial = False + else: + if logical_operator == 'AND': + q_objects &= Q(id__in=nested_queryset.values('id')) + elif logical_operator == 'OR': + q_objects |= Q(id__in=nested_queryset.values('id')) + elif logical_operator == 'NOT': + q_objects &= ~Q(id__in=nested_queryset.values('id')) else: - queryset = queryset.filter(**{key: value}) - return queryset + if initial: + q_objects = Q(**{key: value}) + initial = False + else: + if logical_operator == 'AND': + q_objects &= Q(**{key: value}) + elif logical_operator == 'OR': + q_objects |= Q(**{key: value}) + elif logical_operator == 'NOT': + q_objects &= ~Q(**{key: value}) + + return queryset.filter(q_objects) + + def create(self, validated_data): ''' diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 292466f..7ce4e34 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -7,6 +7,7 @@ from users.views import BasePermissionAPIView from rest_framework.response import Response from rest_framework import status from dynamic_tables.DynamicTable import DynamicTable +from rest_framework.permissions import AllowAny from dynamic_tables.models import DynamicTableReport from .models import Solicitud from users.permisos import es_admin @@ -21,7 +22,7 @@ class SolicitudAPIView(BasePermissionAPIView): herencia: BasePermissionAPIView, clase con los permisos predeterminados para los usuarios ''' - permission_classes_create = [es_admin] + permission_classes_list = [AllowAny] def get(self, request): ''' @@ -45,17 +46,23 @@ class SolicitudAPIView(BasePermissionAPIView): ], exclude_columns=[], search_query=None, - filters={'timestamp__gte':fecha_limite_str} + filters={} ) # Inicializamos un objeto del tipo dynamicTable para obtener la lista serializer = DynamicTable(instance=configuracion_reporte) # Obtenemos los datos usando el método get_data de nuestro serializer - data = serializer.get_data(configuracion_reporte) + data = serializer.get_data(configuracion_reporte, logical_operator="AND") + + # Obtenemos los filtros disponibles a aplicar a los campos enviados + available_filters = serializer.get_available_filters(configuracion_reporte) + + # Creamos un response data para devolver la data y los filtros disponibles + response_data = {'data': data, 'available_filters': available_filters} # Devolvemos la respuesta - return Response(data, status=status.HTTP_200_OK) + return Response(response_data, status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): @@ -81,11 +88,17 @@ class SolicitudAPIView(BasePermissionAPIView): serializer = DynamicTable(data=request.data) if serializer.is_valid(): # Guarda la configuración si no existe ya - report = serializer.save() + configuracion_reporte = serializer.save() # Recupera los datos según la configuración - data = serializer.get_data(report) + data = serializer.get_data(configuracion_reporte, request.data.get('logical_operator')) + + # Recuperamos los filtros disponibles de l configuración enviada + available_filters = serializer.get_available_filters(configuracion_reporte) + + # creamos el response data para enviar la data y los filtros disponibles + response_data = {'data': data, 'available_filters': available_filters} - return Response(data, status=status.HTTP_200_OK) + return Response(response_data, status=status.HTTP_200_OK) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index da9d7c8..2c0d07e 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -38,20 +38,21 @@ class Municipio(models.Model): # modelo para el registro de los datos bancarios del solicitante class DatosBancarios(models.Model): # Choices para el campo de régimen fiscal - REGIMEN_CHOICES= [ - ('1', 'Régminen Simplificado de Confianza'), - ('2', 'Sueldos y salarios e ingresos asimilados a salarios'), - ('3', 'Régimen de Actividades Empresariales y Profesionales'), - ('4', 'Régimen de Incorporación Fiscal'), - ('5', 'Enajenación de bienes'), - ('6', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), - ('7', 'Régimen de Arrendamiento'), - ('8', 'Intereses'), - ('9', 'Obtención de premios'), - ('10', 'Dividendos'), - ('11', 'Demás Ingresos'), - ('12', 'Sin obligaciones fiscales') + REGIMEN_CHOICES = [ + ('Régimen Simplificado de Confianza', 'Régimen Simplificado de Confianza'), + ('Sueldos y salarios e ingresos asimilados a salarios', 'Sueldos y salarios e ingresos asimilados a salarios'), + ('Régimen de Actividades Empresariales y Profesionales', 'Régimen de Actividades Empresariales y Profesionales'), + ('Régimen de Incorporación Fiscal', 'Régimen de Incorporación Fiscal'), + ('Enajenación de bienes', 'Enajenación de bienes'), + ('Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), + ('Régimen de Arrendamiento', 'Régimen de Arrendamiento'), + ('Intereses', 'Intereses'), + ('Obtención de premios', 'Obtención de premios'), + ('Dividendos', 'Dividendos'), + ('Demás Ingresos', 'Demás Ingresos'), + ('Sin obligaciones fiscales', 'Sin obligaciones fiscales') ] + # identificador del objeto id = models.BigAutoField(primary_key=True) # nombre del banco al que pertenece la cuenta -- GitLab From 2e23cb37c1885729985c3d76aabe37d14c2f9812 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 15 Jul 2024 12:47:42 -0600 Subject: [PATCH 3/8] Columnas disponibles en response --- cosiap_api/dynamic_tables/DynamicTable.py | 172 +++++++++++++++------- cosiap_api/solicitudes/views.py | 12 +- 2 files changed, 128 insertions(+), 56 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 7eed6b3..9382213 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -7,6 +7,9 @@ from django.apps import apps from django.db.models import Q, Prefetch from .models import DynamicTableReport from django.db import models +from collections import defaultdict +from django.db.models import ForeignKey, ManyToManyField, ManyToOneRel +import re class DynamicTable(serializers.ModelSerializer): ''' @@ -28,40 +31,35 @@ class DynamicTable(serializers.ModelSerializer): ''' try: - # Primero tenemos que buscar el modelo en las aplicaciones registradas en el sistema - # Para asegurarnos de que el modelo sea único y sea el deseado. - for app_config in apps.get_app_configs(): - try: - model = app_config.get_model(obj.model_name) - break - except LookupError: - continue - else: - # Si no encontramos el modelo enviamos un error - raise serializers.ValidationError(f"Model {obj.model_name} not found.") + model = self.buscar_modelo(obj) columns = obj.columns - exclude_columns = obj.exclude_columns or [] # Si es nulo, convertimos a una lista vacía para el mejor manejo + exclude_columns = obj.exclude_columns or [] search_query = obj.search_query - filters = obj.filters or {} # Si es nulo, convertimos a dict vacío para mejor manejo + filters = obj.filters or {} queryset = model.objects.all() - # Aplicamos los filtros usando la función recursiva + # Aplicar filtros usando la función recursiva queryset = self.apply_filters(queryset, filters, logical_operator) - # Aplicamos el searchquery enviado y extraemos la información de los campos + + # Aplicar búsqueda al queryset if search_query: - search_fields = [field for field in model._meta.fields if field.name in columns] + # Limitar la búsqueda a los campos de tipo cadena + search_fields = [ + field.name for field in model._meta.fields + if isinstance(field, (models.CharField, models.TextField)) + ] search_criteria = Q() - for field in search_fields: - search_criteria |= Q(**{f"{field.name}__icontains": search_query}) + for field_name in search_fields: + search_criteria |= Q(**{f"{field_name}__icontains": search_query}) queryset = queryset.filter(search_criteria) - # Seleccionamos las columnas a incluir y las columnas a excluir + # Seleccionar columnas a incluir y excluir queryset = queryset.values(*[col for col in columns if col not in exclude_columns]) - # Finalmente incluimos todos los campos que estén relacionados a los modelos por las llaves foráneas + # Incluir campos relacionados for field in model._meta.get_fields(): if (field.is_relation and (field.name not in exclude_columns) and @@ -69,13 +67,28 @@ class DynamicTable(serializers.ModelSerializer): related_queryset = field.related_model.objects.all() queryset = queryset.prefetch_related(Prefetch(field.name, queryset=related_queryset)) - # Devolvemos el nuevo queryset con filtros y exclusiones realizadas return list(queryset) except Exception as e: - # Si hay algun fallo dentro de la lógica de la extracción de datos, regresamos una lista vacía + # Si hay algun fallo, regresamos una lista vacía return [] + @staticmethod + def buscar_modelo(obj): + ''' + Método para obtener el modelo de la base de datos, buscando en las aplicaciones registradas. + ''' + for app_config in apps.get_app_configs(): + try: + model = app_config.get_model(obj.model_name) + break + except LookupError: + model = None + else: + model = None + raise serializers.ValidationError(f"Model {obj.model_name} not found.") + return model + def get_available_filters(self, obj): ''' Método para obtener los filtros disponibles a aplicar a una configuración de tabla dinámica @@ -86,15 +99,7 @@ class DynamicTable(serializers.ModelSerializer): ''' available_filters = {} - # Buscamos el modelo en las aplicaciones registradas - for app_config in apps.get_app_configs(): - try: - model = app_config.get_model(obj.model_name) - break - except LookupError: - continue - else: - raise serializers.ValidationError(f"Model {obj.model_name} not found.") + model = self.buscar_modelo(obj) for column in obj.columns: field_path = column.split('__') @@ -116,6 +121,67 @@ class DynamicTable(serializers.ModelSerializer): return available_filters + + def get_available_columns(self, obj): + ''' + Método para obtener las columnas disponibles a solicitar de un determinado modelo (para la utilización de filtros y exclusiones) + + parámetros: + - obj: objeto de configuración de tabla dinámica + ''' + available_columns = {} + + model = self.buscar_modelo(obj) + if model is None: + # En caso de que no se haya encontrado el modelo enviamos un diccionario vacío + return available_columns + + available_columns = self.get_model_fields(model, set()) + return available_columns + + + + @staticmethod + def get_model_fields(model, visited_models): + ''' + Método recursivo para obtener los campos de un modelo, inclusive los relacionados, cuidando las relaciones muchos a muchos + + parámetros: + - model: modelo del cual obtener las columnas + - visited_models: conjunto para determinar si un modelo ya fue checkeado o no + ''' + if model in visited_models: + return {} + + # Una vez el modelo sea visitado lo guardamos en la lista + visited_models.add(model) + fields = {} + + # Expresión regular para excluir columnas específicas + exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at', re.IGNORECASE) + + for field in model._meta.get_fields(): + # Verificar si el campo debe ser excluido + if exclude_pattern.search(field.name): + continue + + if isinstance(field, ForeignKey): + related_model = field.related_model + related_fields = DynamicTable.get_model_fields(related_model, visited_models) + for related_field_name, related_field_verbose_name in related_fields.items(): + if not exclude_pattern.search(related_field_name): + fields[f"{field.name}__{related_field_name}"] = f"{related_field_verbose_name}" + elif isinstance(field, ManyToManyField): + fields[field.name] = field.verbose_name + elif isinstance(field, ManyToOneRel): + continue # Omitir relaciones inversas + else: + fields[field.name] = field.verbose_name + + visited_models.remove(model) + return fields + + @staticmethod def apply_filters(queryset, filters, logical_operator): """ @@ -134,31 +200,31 @@ class DynamicTable(serializers.ModelSerializer): q_objects = Q() initial = True # Flag para el primer filtro + # Agrupar filtros por campo (Para determinar si podemos aplicar el OR a un mismo campo en todos los casos) + field_filters = defaultdict(list) for key, value in filters.items(): - if isinstance(value, dict): - nested_queryset = queryset.filter(**{f"{key}__isnull": False}) - nested_queryset = DynamicTable.apply_filters(nested_queryset, value, logical_operator) - if initial: - q_objects = Q(id__in=nested_queryset.values('id')) - initial = False + field_filters[key].append(value) + + for field, values in field_filters.items(): + field_q_objects = Q() + for value in values: + if isinstance(value, dict): + nested_queryset = queryset.filter(**{f"{field}__isnull": False}) + nested_queryset = DynamicTable.apply_filters(nested_queryset, value, logical_operator) + field_q_objects |= Q(id__in=nested_queryset.values('id')) else: - if logical_operator == 'AND': - q_objects &= Q(id__in=nested_queryset.values('id')) - elif logical_operator == 'OR': - q_objects |= Q(id__in=nested_queryset.values('id')) - elif logical_operator == 'NOT': - q_objects &= ~Q(id__in=nested_queryset.values('id')) + field_q_objects |= Q(**{field: value}) + + if initial: + q_objects = field_q_objects + initial = False else: - if initial: - q_objects = Q(**{key: value}) - initial = False - else: - if logical_operator == 'AND': - q_objects &= Q(**{key: value}) - elif logical_operator == 'OR': - q_objects |= Q(**{key: value}) - elif logical_operator == 'NOT': - q_objects &= ~Q(**{key: value}) + if logical_operator == 'AND': + q_objects &= field_q_objects + elif logical_operator == 'OR': + q_objects |= field_q_objects + elif logical_operator == 'NOT': + q_objects &= ~field_q_objects return queryset.filter(q_objects) diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 7ce4e34..ccfab28 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -58,8 +58,11 @@ class SolicitudAPIView(BasePermissionAPIView): # Obtenemos los filtros disponibles a aplicar a los campos enviados available_filters = serializer.get_available_filters(configuracion_reporte) + # Obtenemos las columnas disponibles para nuestro modelo + available_columns = serializer.get_available_columns(configuracion_reporte) + # Creamos un response data para devolver la data y los filtros disponibles - response_data = {'data': data, 'available_filters': available_filters} + response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} # Devolvemos la respuesta return Response(response_data, status=status.HTTP_200_OK) @@ -91,13 +94,16 @@ class SolicitudAPIView(BasePermissionAPIView): configuracion_reporte = serializer.save() # Recupera los datos según la configuración - data = serializer.get_data(configuracion_reporte, request.data.get('logical_operator')) + data = serializer.get_data(configuracion_reporte, request.data.get('logical_operator', 'AND')) # Recuperamos los filtros disponibles de l configuración enviada available_filters = serializer.get_available_filters(configuracion_reporte) + # Obtenemos las columnas disponibles de nuestro modelo + available_columns = serializer.get_available_columns(configuracion_reporte) + # creamos el response data para enviar la data y los filtros disponibles - response_data = {'data': data, 'available_filters': available_filters} + response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} return Response(response_data, status=status.HTTP_200_OK) else: -- GitLab From 7fc60f798757178daa1d864ad529ea5461fb2a65 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 16 Jul 2024 13:55:10 -0600 Subject: [PATCH 4/8] =?UTF-8?q?Estructura=20de=20filtros=20adecuada=20a=20?= =?UTF-8?q?como=20se=20solicit=C3=B3,=20funciones=20de=20obtencion=20de=20?= =?UTF-8?q?campos=20de=20tipo=20texto=20para=20el=20manejo=20del=20searchq?= =?UTF-8?q?uery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 211 ++++++++++++------ ...lumna_excludefilters_dynamictablereport.py | 22 ++ ...lumna_excludefilters_dynamictablereport.py | 18 ++ cosiap_api/dynamic_tables/models.py | 1 + cosiap_api/solicitudes/views.py | 22 +- ...lumna_excludefilters_dynamictablereport.py | 18 ++ cosiap_api/users/models.py | 24 +- 7 files changed, 219 insertions(+), 97 deletions(-) create mode 100644 cosiap_api/dynamic_tables/migrations/0002_columna_excludefilters_dynamictablereport.py create mode 100644 cosiap_api/dynamic_tables/migrations/0003_columna_excludefilters_dynamictablereport.py create mode 100644 cosiap_api/users/migrations/0011_columna_excludefilters_dynamictablereport.py diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 9382213..20ed597 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -21,15 +21,7 @@ class DynamicTable(serializers.ModelSerializer): model = DynamicTableReport fields = '__all__' - def get_data(self, obj, logical_operator): - ''' - Método que se encargará de recuperar los datos correspondientes del modelo enviado - - parámetros: - - obj: Configuración de reporte recibida, la cuál indica que datos se van a recuperar - - logical_operator: operador lógico para la aplicación de los filtros - ''' - + def get_data(self, obj): try: model = self.buscar_modelo(obj) @@ -37,31 +29,40 @@ class DynamicTable(serializers.ModelSerializer): exclude_columns = obj.exclude_columns or [] search_query = obj.search_query filters = obj.filters or {} + exclude_filters = obj.exclude_filters or {} + + # Obtener los filtros disponibles para este modelo + available_filters = self.get_available_filters(obj) + + # Validar y limpiar los filtros enviados por la request + clean_filters = self.validate_and_clean_filters({'filters': filters}, available_filters) queryset = model.objects.all() # Aplicar filtros usando la función recursiva - queryset = self.apply_filters(queryset, filters, logical_operator) + queryset = self.apply_filters(queryset, clean_filters, exclude_filters) + + # Obtener los campos de tipo CharField o TextField del modelo y modelos relacionados + char_and_text_fields = self.get_char_and_text_fields(model) + related_char_and_text_fields = self.get_related_char_and_text_fields(model) - - # Aplicar búsqueda al queryset + # Aplicar búsqueda en campos de tipo CharField o TextField mediante el search_query if search_query: - # Limitar la búsqueda a los campos de tipo cadena - search_fields = [ - field.name for field in model._meta.fields - if isinstance(field, (models.CharField, models.TextField)) - ] - search_criteria = Q() - for field_name in search_fields: - search_criteria |= Q(**{f"{field_name}__icontains": search_query}) - queryset = queryset.filter(search_criteria) + search_filters = [] + for field in char_and_text_fields + related_char_and_text_fields: + search_filters.append(Q(**{f"{field}__icontains": search_query})) + search_filter = Q() + for sf in search_filters: + search_filter |= sf + queryset = queryset.filter(search_filter) + # Seleccionar columnas a incluir y excluir queryset = queryset.values(*[col for col in columns if col not in exclude_columns]) # Incluir campos relacionados for field in model._meta.get_fields(): - if (field.is_relation and + if (field.is_relation and (field.name not in exclude_columns) and (field.related_model is not None)): related_queryset = field.related_model.objects.all() @@ -69,7 +70,7 @@ class DynamicTable(serializers.ModelSerializer): return list(queryset) except Exception as e: - # Si hay algun fallo, regresamos una lista vacía + # Si hay algún fallo, regresamos una lista vacía return [] @@ -89,38 +90,119 @@ class DynamicTable(serializers.ModelSerializer): raise serializers.ValidationError(f"Model {obj.model_name} not found.") return model + def get_available_filters(self, obj): ''' Método para obtener los filtros disponibles a aplicar a una configuración de tabla dinámica con la finalidad de que dichos filtros puedan ser seleccionados desde el frontend parámetros: - - obj: objeto de con la configuración actual de la tabla dinámica + - obj: objeto con la configuración actual de la tabla dinámica ''' - available_filters = {} + available_filters = [] model = self.buscar_modelo(obj) - + for column in obj.columns: field_path = column.split('__') field = model - + label = "" for part in field_path: - field = field._meta.get_field(part).related_model if field._meta.get_field(part).is_relation else field._meta.get_field(part) - + if field._meta.get_field(part).is_relation: + field = field._meta.get_field(part).related_model + else: + field = field._meta.get_field(part) + label = field.verbose_name.capitalize() + + filter_info = { + 'campo': column, + 'label': label or field.verbose_name.capitalize(), + 'html_type': '', + 'lookups': [], + } + # Verificamos el tipo de campo actual para poder enviar los filtros disponibles + # correctos según el tipo de campo, y el html_value correspondiente if isinstance(field, models.CharField) or isinstance(field, models.TextField): + filter_info['html_type'] = 'textInput' + filter_info['lookups'] = ['icontains'] + # verificamos si existen choices para crear un nuevo diccionario "exact" con las keys y values if field.choices: - available_filters[column] = [choice[0] for choice in field.choices] - else: - available_filters[column] = ['icontains'] + filter_info['lookups'] = ['iexact'] + filter_info['choices'] = [{'label': choice[1], 'value': choice[0]} for choice in field.choices] elif isinstance(field, models.IntegerField) or isinstance(field, models.FloatField) or isinstance(field, models.DecimalField): - available_filters[column] = ['gt', 'lt'] + filter_info['html_type'] = 'numberInput' + filter_info['lookups'] = ['gt', 'lt'] elif isinstance(field, models.DateField) or isinstance(field, models.DateTimeField): - available_filters[column] = ['date_min', 'date_max'] - + filter_info['html_type'] = 'dateInput' + filter_info['lookups'] = ['date_min', 'date_max'] + + available_filters.append(filter_info) + return available_filters + @staticmethod + def validate_and_clean_filters(request_data, available_filters): + ''' + Función para validar y limpiar los filtros que nos ayudará a asegurarnos de que solo + se apliquen los filtros permitidos y limitar la cantidad de valores. + + parámetros: + - request_data: datos enviados en la solicitud + - available_filters: filtros disponibles previamente obtenidos + ''' + clean_filters = {} + + for filtro_d in available_filters: + campo = filtro_d['campo'] + if campo in request_data['filters']: + # limpiamos el filtro + clean_filters[campo] = {} + for lookup in filtro_d['lookups']: + if lookup in request_data['filters'][campo]: + # vamos a poner un tope de 100 valores para evitar saturación + clean_filters[campo][lookup] = request_data['filters'][campo][lookup][:100] + + return clean_filters + + def get_char_and_text_fields(self, model): + """ + Obtiene todos los campos de tipo CharField o TextField del modelo y sus campos relacionados. + + :param model: Modelo del cual obtener los campos. + :return: Lista de nombres de campos de tipo CharField o TextField. + """ + fields = [] + + for field in model._meta.get_fields(): + if isinstance(field, (models.CharField, models.TextField)): + fields.append(field.name) + elif isinstance(field, ForeignKey): + related_model = field.related_model + related_fields = self.get_char_and_text_fields(related_model) + fields.extend([f"{field.name}__{related_field}" for related_field in related_fields]) + + return fields + + + def get_related_char_and_text_fields(self, model): + """ + Obtiene todos los campos de tipo CharField o TextField de los modelos relacionados a través de llaves foráneas. + + :param model: Modelo del cual obtener los campos relacionados. + :return: Lista de nombres de campos de tipo CharField o TextField de modelos relacionados. + """ + fields = [] + + for field in model._meta.get_fields(): + if isinstance(field, ForeignKey): + related_model = field.related_model + related_fields = self.get_char_and_text_fields(related_model) + fields.extend([f"{field.name}__{related_field}" for related_field in related_fields]) + + return fields + + def get_available_columns(self, obj): ''' @@ -183,50 +265,45 @@ class DynamicTable(serializers.ModelSerializer): @staticmethod - def apply_filters(queryset, filters, logical_operator): + def apply_filters(queryset, filters, exclude_filters): """ Aplica filtros recursivamente a un queryset basado en un diccionario de filtros - y un operador lógico. + y exclusiones. :param queryset: Queryset original al que se le aplican los filtros. :param filters: Diccionario de filtros donde las claves son los campos y los valores son los criterios de filtro. - :param logical_operator: Operador lógico a aplicar ('AND', 'OR', 'NOT') + :param exclude_filters: Diccionario de filtros de exclusión donde las claves son los campos y los valores son los criterios de filtro. - :return: Queryset filtrado según los filtros y el operador lógico especificado. + :return: Queryset filtrado según los filtros y las exclusiones especificadas. """ - if not filters: + if not filters and not exclude_filters: return queryset q_objects = Q() - initial = True # Flag para el primer filtro - - # Agrupar filtros por campo (Para determinar si podemos aplicar el OR a un mismo campo en todos los casos) - field_filters = defaultdict(list) - for key, value in filters.items(): - field_filters[key].append(value) + exclude_q_objects = Q() - for field, values in field_filters.items(): + # Aplicar filtros (OR dentro de un mismo campo, AND entre diferentes campos) + for field, lookup_values in filters.items(): field_q_objects = Q() - for value in values: - if isinstance(value, dict): - nested_queryset = queryset.filter(**{f"{field}__isnull": False}) - nested_queryset = DynamicTable.apply_filters(nested_queryset, value, logical_operator) - field_q_objects |= Q(id__in=nested_queryset.values('id')) - else: - field_q_objects |= Q(**{field: value}) - - if initial: - q_objects = field_q_objects - initial = False - else: - if logical_operator == 'AND': - q_objects &= field_q_objects - elif logical_operator == 'OR': - q_objects |= field_q_objects - elif logical_operator == 'NOT': - q_objects &= ~field_q_objects - - return queryset.filter(q_objects) + for lookup, values in lookup_values.items(): + lookup_q_objects = Q() + for value in values: + lookup_q_objects |= Q(**{f"{field}__{lookup}": value}) + field_q_objects |= lookup_q_objects # OR dentro de un mismo campo + q_objects &= field_q_objects # AND entre diferentes campos + + # Aplicar filtros de exclusión (AND para todos los campos) + for field, lookup_values in exclude_filters.items(): + field_exclude_q_objects = Q() + for lookup, values in lookup_values.items(): + for value in values: + field_exclude_q_objects &= ~Q(**{f"{field}__{lookup}": value}) + exclude_q_objects &= field_exclude_q_objects + + # Aplicar los filtros al queryset original + queryset = queryset.filter(q_objects).exclude(exclude_q_objects) + + return queryset diff --git a/cosiap_api/dynamic_tables/migrations/0002_columna_excludefilters_dynamictablereport.py b/cosiap_api/dynamic_tables/migrations/0002_columna_excludefilters_dynamictablereport.py new file mode 100644 index 0000000..2a0743c --- /dev/null +++ b/cosiap_api/dynamic_tables/migrations/0002_columna_excludefilters_dynamictablereport.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.6 on 2024-07-16 17:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_tables', '0001_creacion_tabla_dynamictablereport'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='dynamictablereport', + unique_together=set(), + ), + migrations.AddField( + model_name='dynamictablereport', + name='exlcude_filters', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/cosiap_api/dynamic_tables/migrations/0003_columna_excludefilters_dynamictablereport.py b/cosiap_api/dynamic_tables/migrations/0003_columna_excludefilters_dynamictablereport.py new file mode 100644 index 0000000..0903dd7 --- /dev/null +++ b/cosiap_api/dynamic_tables/migrations/0003_columna_excludefilters_dynamictablereport.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-16 17:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamic_tables', '0002_columna_excludefilters_dynamictablereport'), + ] + + operations = [ + migrations.RenameField( + model_name='dynamictablereport', + old_name='exlcude_filters', + new_name='exclude_filters', + ), + ] diff --git a/cosiap_api/dynamic_tables/models.py b/cosiap_api/dynamic_tables/models.py index e750574..899b59b 100644 --- a/cosiap_api/dynamic_tables/models.py +++ b/cosiap_api/dynamic_tables/models.py @@ -16,6 +16,7 @@ class DynamicTableReport(models.Model): exclude_columns = models.JSONField(blank=True, null=True) search_query = models.CharField(max_length=100, blank=True, null=True) filters = models.JSONField(blank=True, null=True) + exclude_filters = models.JSONField(blank=True, null=True) def __str__(self): return 'Tabla Dinámica: ' + self.model_name diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index ccfab28..a8b1621 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -46,14 +46,15 @@ class SolicitudAPIView(BasePermissionAPIView): ], exclude_columns=[], search_query=None, - filters={} + filters={}, + exclude_filters = {} ) # Inicializamos un objeto del tipo dynamicTable para obtener la lista serializer = DynamicTable(instance=configuracion_reporte) # Obtenemos los datos usando el método get_data de nuestro serializer - data = serializer.get_data(configuracion_reporte, logical_operator="AND") + data = serializer.get_data(configuracion_reporte) # Obtenemos los filtros disponibles a aplicar a los campos enviados available_filters = serializer.get_available_filters(configuracion_reporte) @@ -71,21 +72,6 @@ class SolicitudAPIView(BasePermissionAPIView): def post(self, request, *args, **kwargs): ''' En la solicitud post manejamos el caso del envio de filtros, exclusion de columnas y searchquerys - - - Ejemplo para envío JSON con filtros: (Para comprender mejor como vienen los datos en el request) - - - Obtención de todas las solicitudes cuyo monto aprobado sea mayor a 10000: - - { - "model_name": "Solicitud", - "columns": ["status", "solicitud_n", "minuta", "convenio", "monto_solicitado", "monto_aprobado", "modalidad", "timestamp", "observacion", "solicitante"], - "exclude_columns": [], - "search_query": "", - "filters": { - "monto_aprobado__gt": 10000 - } - } ''' # Debemos inicializar el serializer con la request enviada serializer = DynamicTable(data=request.data) @@ -94,7 +80,7 @@ class SolicitudAPIView(BasePermissionAPIView): configuracion_reporte = serializer.save() # Recupera los datos según la configuración - data = serializer.get_data(configuracion_reporte, request.data.get('logical_operator', 'AND')) + data = serializer.get_data(configuracion_reporte) # Recuperamos los filtros disponibles de l configuración enviada available_filters = serializer.get_available_filters(configuracion_reporte) diff --git a/cosiap_api/users/migrations/0011_columna_excludefilters_dynamictablereport.py b/cosiap_api/users/migrations/0011_columna_excludefilters_dynamictablereport.py new file mode 100644 index 0000000..b9e8414 --- /dev/null +++ b/cosiap_api/users/migrations/0011_columna_excludefilters_dynamictablereport.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-16 17:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_creacion_modulos_admin_formats_forms_tables_modalidades_solicitudes'), + ] + + operations = [ + migrations.AlterField( + model_name='datosbancarios', + name='regimen', + field=models.CharField(choices=[('1', 'Régimen Simplificado de Confianza'), ('2', 'Sueldos y salarios e ingresos asimilados a salarios'), ('3', 'Régimen de Actividades Empresariales y Profesionales'), ('4', 'Régimen de Incorporación Fiscal'), ('5', 'Enajenación de bienes'), ('6', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), ('7', 'Régimen de Arrendamiento'), ('8', 'Intereses'), ('9', 'Obtención de premios'), ('10', 'Dividendos'), ('11', 'Demás Ingresos'), ('12', 'Sin obligaciones fiscales')], max_length=255), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index 2c0d07e..fc271b8 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -39,18 +39,18 @@ class Municipio(models.Model): class DatosBancarios(models.Model): # Choices para el campo de régimen fiscal REGIMEN_CHOICES = [ - ('Régimen Simplificado de Confianza', 'Régimen Simplificado de Confianza'), - ('Sueldos y salarios e ingresos asimilados a salarios', 'Sueldos y salarios e ingresos asimilados a salarios'), - ('Régimen de Actividades Empresariales y Profesionales', 'Régimen de Actividades Empresariales y Profesionales'), - ('Régimen de Incorporación Fiscal', 'Régimen de Incorporación Fiscal'), - ('Enajenación de bienes', 'Enajenación de bienes'), - ('Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), - ('Régimen de Arrendamiento', 'Régimen de Arrendamiento'), - ('Intereses', 'Intereses'), - ('Obtención de premios', 'Obtención de premios'), - ('Dividendos', 'Dividendos'), - ('Demás Ingresos', 'Demás Ingresos'), - ('Sin obligaciones fiscales', 'Sin obligaciones fiscales') + ('1', 'Régimen Simplificado de Confianza'), + ('2', 'Sueldos y salarios e ingresos asimilados a salarios'), + ('3', 'Régimen de Actividades Empresariales y Profesionales'), + ('4', 'Régimen de Incorporación Fiscal'), + ('5', 'Enajenación de bienes'), + ('6', 'Régimen de Actividades Empresariales con ingresos a través de Plataformas Tecnológicas'), + ('7', 'Régimen de Arrendamiento'), + ('8', 'Intereses'), + ('9', 'Obtención de premios'), + ('10', 'Dividendos'), + ('11', 'Demás Ingresos'), + ('12', 'Sin obligaciones fiscales') ] # identificador del objeto -- GitLab From 8bb4452d8964f50d43aa0756973639f8cd8901da Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 16 Jul 2024 17:52:42 -0600 Subject: [PATCH 5/8] =?UTF-8?q?Filtros=20de=20exclusi=C3=B3n=20a=C3=B1adid?= =?UTF-8?q?os=20a=20modelo=20y=20l=C3=B3gica=20implementada,=20test=20agre?= =?UTF-8?q?gados=20a=20solicitudes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 32 ++-- cosiap_api/notificaciones/tests.py | 2 +- cosiap_api/solicitudes/tests.py | 217 +++++++++++++++++++++- cosiap_api/users/models.py | 4 +- 4 files changed, 240 insertions(+), 15 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 20ed597..62e2cf8 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -22,6 +22,12 @@ class DynamicTable(serializers.ModelSerializer): fields = '__all__' def get_data(self, obj): + ''' + Clase para obtener los datos solicitados en la configuración de reporte enviada + + param: + - obj: Configuración de reporte que contiene las columnas solicitadas, los filtros, exclusiones, etc... + ''' try: model = self.buscar_modelo(obj) @@ -269,19 +275,19 @@ class DynamicTable(serializers.ModelSerializer): """ Aplica filtros recursivamente a un queryset basado en un diccionario de filtros y exclusiones. - + :param queryset: Queryset original al que se le aplican los filtros. :param filters: Diccionario de filtros donde las claves son los campos y los valores son los criterios de filtro. :param exclude_filters: Diccionario de filtros de exclusión donde las claves son los campos y los valores son los criterios de filtro. - + :return: Queryset filtrado según los filtros y las exclusiones especificadas. """ if not filters and not exclude_filters: return queryset - + q_objects = Q() exclude_q_objects = Q() - + # Aplicar filtros (OR dentro de un mismo campo, AND entre diferentes campos) for field, lookup_values in filters.items(): field_q_objects = Q() @@ -289,22 +295,24 @@ class DynamicTable(serializers.ModelSerializer): lookup_q_objects = Q() for value in values: lookup_q_objects |= Q(**{f"{field}__{lookup}": value}) - field_q_objects |= lookup_q_objects # OR dentro de un mismo campo + field_q_objects &= lookup_q_objects # AND dentro de un mismo campo q_objects &= field_q_objects # AND entre diferentes campos - - # Aplicar filtros de exclusión (AND para todos los campos) + + # Aplicar filtros de exclusión (OR dentro de un mismo campo, AND entre diferentes campos) for field, lookup_values in exclude_filters.items(): field_exclude_q_objects = Q() for lookup, values in lookup_values.items(): + lookup_exclude_q_objects = Q() for value in values: - field_exclude_q_objects &= ~Q(**{f"{field}__{lookup}": value}) - exclude_q_objects &= field_exclude_q_objects - + lookup_exclude_q_objects |= Q(**{f"{field}__{lookup}": value}) + field_exclude_q_objects |= lookup_exclude_q_objects # OR dentro de un mismo campo + exclude_q_objects &= field_exclude_q_objects # AND entre diferentes campos + # Aplicar los filtros al queryset original queryset = queryset.filter(q_objects).exclude(exclude_q_objects) - + return queryset - + def create(self, validated_data): diff --git a/cosiap_api/notificaciones/tests.py b/cosiap_api/notificaciones/tests.py index 622e1de..461f3b6 100644 --- a/cosiap_api/notificaciones/tests.py +++ b/cosiap_api/notificaciones/tests.py @@ -26,7 +26,7 @@ class MensajeTestCase(APITestCase): self.admin_token = self.get_tokens_for_user(self.admin_user) # URL para la vista usuario - self.url = reverse('users:usuario-lista-crear') + self.url = reverse('users:usuario-list-create') def get_tokens_for_user(self, user): refresh = RefreshToken.for_user(user) diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 7ce503c..9db8c30 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -1,3 +1,218 @@ +# tests.py from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from users.models import Usuario, Solicitante, Municipio, Estado +from .models import Solicitud +from django.utils import timezone -# Create your tests here. + +class UsuarioTests(TestCase): + ''' + Clase de prueba de la lista de solicitudes usando DynamicTable + ''' + + def setUp(self): + """Configurar el entorno de prueba""" + self.client = APIClient() + self.usuario_data = { + 'curp': 'CEVA020423HGRRZDA8', + 'nombre': 'Adalberto', + 'email': 'adalc3488@gmail.com', + 'password': 'testpassword123' + } + self.usuario = Usuario.objects.create_superuser(**self.usuario_data) + self.usuario.is_active = True + self.usuario.save() + + # Iniciar sesión + self.login_url = reverse('users:token_obtain') + response = self.client.post(self.login_url, { + 'curp': self.usuario_data['curp'], + 'password': self.usuario_data['password'] + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.access_token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}') + + # Configurar cookie de refresh token + refresh_token = response.cookies.get('refresh_token') + if refresh_token: + self.client.cookies['refresh_token'] = refresh_token.value + + # Crear una instancia de Estado + self.estado = Estado.objects.create(nombre="Test Estado") + + # Crear una instancia de Municipio + self.municipio = Municipio.objects.create( + estado=self.estado, + cve_mun=1, + nombre="Test Municipio" + ) + + # Crear una instancia de Solicitante + self.solicitante = Solicitante.objects.create( + curp="CEVA020423HGRRZDA9", + email="ceva@example.com", + nombre="Adalberto", + ap_paterno="Evans", + ap_materno="Vargas", + telefono="1234567890", + RFC="CEVA0204237E4", + direccion="Calle Falsa 123", + codigo_postal="12345", + municipio= self.municipio, + poblacion="Test Poblacion", + datos_bancarios=None, # Asignar datos bancarios si es necesario + INE=None # Asignar archivo de INE si es necesario + ) + + # Crear instancias de Solicitud usando la instancia de Solicitante + self.solicitud1 = Solicitud.objects.create( + solicitante=self.solicitante, + status="Pendiente", + solicitud_n="001", + timestamp=timezone.now(), + monto_solicitado=1000, + monto_aprobado=800, + observacion="Observación 1", + # Asignar otros campos necesarios + ) + + self.solicitud2 = Solicitud.objects.create( + solicitante=self.solicitante, + status="Aprobado", + solicitud_n="002", + timestamp=timezone.now(), + monto_solicitado=1500, + monto_aprobado=1200, + observacion="Observación 2", + # Asignar otros campos necesarios + ) + + def test_get_solicitudes(self): + """ + Probar que se pueden recuperar todas las solicitudes con una solicitud GET + """ + url = reverse('solicitudes:solicitud-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertEqual(len(response.data['data']), 2) + + def test_post_solicitudes_exclusion_columnas(self): + """ + Probar la exclusión de columnas con una solicitud POST + """ + url = reverse('solicitudes:solicitud-list') + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [ + "monto_solicitado" + ], + "filters": {}, + "exclude_filters": {}, + "search_query": "" + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertNotIn('monto_solicitado', response.data['data'][0]) + + def test_post_solicitudes_con_filtros(self): + """ + Probar la aplicación de filtros con una solicitud POST + """ + url = reverse('solicitudes:solicitud-list') + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [], + "filters": { + "status": { + "icontains": ["Pendiente"] + } + }, + "exclude_filters": {}, + "search_query": "" + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertEqual(len(response.data['data']), 1) + self.assertEqual(response.data['data'][0]['status'], 'Pendiente') + + def test_post_solicitudes_con_filtros_exclusion(self): + """ + Probar la aplicación de filtros de exclusión con una solicitud POST + """ + url = reverse('solicitudes:solicitud-list') + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [], + "filters": {}, + "exclude_filters": { + "status": { + "icontains": ["Pendiente"] + } + }, + "search_query": "" + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertEqual(len(response.data['data']), 1) + self.assertEqual(response.data['data'][0]['status'], 'Aprobado') + + def test_post_solicitudes_con_search_query(self): + """ + Probar la aplicación de un search_query con una solicitud POST + """ + url = reverse('solicitudes:solicitud-list') + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [], + "filters": {}, + "exclude_filters": {}, + "search_query": "Adalberto" + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertEqual(len(response.data['data']), 2) + self.assertEqual(response.data['data'][0]['solicitante__nombre'], 'Adalberto') + + + diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index fc271b8..e512669 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -125,8 +125,10 @@ class Usuario(AbstractBaseUser, PermissionsMixin): ordering = ['-is_superuser', 'id' ] -# clase Solicitante que hereda de Usuario class Solicitante(Usuario): + ''' + Calse para representar al solicitante (Hereda de Usuario) + ''' # validador para el formato del RFC RFC_REGEX = r'^([A-ZÑ&]{3,4}) ?(?:- ?)?(\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])) ?(?:- ?)?([A-Z\d]{2})([A\d])$' -- GitLab From e6558b51f202e9b8c784fd329c99db0f44abe26e Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 17 Jul 2024 11:22:31 -0600 Subject: [PATCH 6/8] =?UTF-8?q?evitar=20creaci=C3=B3n=20de=20objeto=20repe?= =?UTF-8?q?tido=20en=20metodo=20get=20de=20SolicituAPIView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/solicitudes/tests.py | 35 +++++++++++++++++++++++++++++++++ cosiap_api/solicitudes/views.py | 20 ++++++++++--------- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 9db8c30..f18b448 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -24,6 +24,7 @@ class UsuarioTests(TestCase): } self.usuario = Usuario.objects.create_superuser(**self.usuario_data) self.usuario.is_active = True + self.usuario.is_staff = True self.usuario.save() # Iniciar sesión @@ -158,6 +159,40 @@ class UsuarioTests(TestCase): self.assertEqual(len(response.data['data']), 1) self.assertEqual(response.data['data'][0]['status'], 'Pendiente') + + def test_post_solicitudes_con_filtros_or(self): + """ + Probar la aplicación de filtros OR con una solicitud POST + """ + url = reverse('solicitudes:solicitud-list') + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [], + "filters": { + "status": { + "icontains": ["Pendiente"] + }, + "solicitante__nombre":{ + "icontains": ["Adalberto", "Nombre_inexistente"] + } + }, + "exclude_filters": {}, + "search_query": "" + } + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + self.assertEqual(len(response.data['data']), 1) + self.assertEqual(response.data['data'][0]['status'], 'Pendiente') + def test_post_solicitudes_con_filtros_exclusion(self): """ Probar la aplicación de filtros de exclusión con una solicitud POST diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index a8b1621..c3e3f20 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -28,12 +28,11 @@ class SolicitudAPIView(BasePermissionAPIView): ''' En la solicitud get manejamos la lógica para la lista de solicitudes mediante la tabla dinámica ''' - # necesitamos unicamente las solicitudes realizadas hace 5 meses + # Necesitamos únicamente las solicitudes realizadas hace 5 meses fecha_limite = timezone.now() - timedelta(days=5 * 30) # 5 meses estimados en días - # Convertimos fecha_limite a una cadena formateada - fecha_limite_str = fecha_limite.strftime('%Y-%m-%d %H:%M:%S') - # Si aún no se ha creado el reporte por defecto (con toda la información), lo creamos - configuracion_reporte, created = DynamicTableReport.objects.get_or_create( + + # Construimos la configuración del reporte sin guardarlo en la base de datos (para que no se guarde cada vez que se hace una nueva solicitud) + configuracion_reporte = DynamicTableReport( model_name='Solicitud', columns=[ 'status', 'solicitud_n', 'minuta__archivo', 'convenio__archivo', 'monto_solicitado', 'monto_aprobado', @@ -46,11 +45,14 @@ class SolicitudAPIView(BasePermissionAPIView): ], exclude_columns=[], search_query=None, - filters={}, + filters={ + "timestamp": { + "date_max": [fecha_limite] + }}, exclude_filters = {} - ) - - # Inicializamos un objeto del tipo dynamicTable para obtener la lista + ) + + # Inicializamos un objeto del tipo DynamicTable para obtener la lista serializer = DynamicTable(instance=configuracion_reporte) # Obtenemos los datos usando el método get_data de nuestro serializer -- GitLab From 6a9194a11af8c917f706a36b33ebd2adb9c944e9 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 17 Jul 2024 11:49:12 -0600 Subject: [PATCH 7/8] =?UTF-8?q?Solicitudes=205=20meses=20atr=C3=A1s=20pend?= =?UTF-8?q?iente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/solicitudes/tests.py | 4 +--- cosiap_api/solicitudes/views.py | 12 ++++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index f18b448..5cf35fe 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -8,7 +8,7 @@ from .models import Solicitud from django.utils import timezone -class UsuarioTests(TestCase): +class SolicitudTests(TestCase): ''' Clase de prueba de la lista de solicitudes usando DynamicTable ''' @@ -74,7 +74,6 @@ class UsuarioTests(TestCase): solicitante=self.solicitante, status="Pendiente", solicitud_n="001", - timestamp=timezone.now(), monto_solicitado=1000, monto_aprobado=800, observacion="Observación 1", @@ -85,7 +84,6 @@ class UsuarioTests(TestCase): solicitante=self.solicitante, status="Aprobado", solicitud_n="002", - timestamp=timezone.now(), monto_solicitado=1500, monto_aprobado=1200, observacion="Observación 2", diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index c3e3f20..dc5ce8c 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -22,15 +22,14 @@ class SolicitudAPIView(BasePermissionAPIView): herencia: BasePermissionAPIView, clase con los permisos predeterminados para los usuarios ''' - permission_classes_list = [AllowAny] + permission_classes_list = [es_admin] + permission_classes_create = [es_admin] def get(self, request): ''' En la solicitud get manejamos la lógica para la lista de solicitudes mediante la tabla dinámica ''' - # Necesitamos únicamente las solicitudes realizadas hace 5 meses - fecha_limite = timezone.now() - timedelta(days=5 * 30) # 5 meses estimados en días - + # Construimos la configuración del reporte sin guardarlo en la base de datos (para que no se guarde cada vez que se hace una nueva solicitud) configuracion_reporte = DynamicTableReport( model_name='Solicitud', @@ -45,10 +44,7 @@ class SolicitudAPIView(BasePermissionAPIView): ], exclude_columns=[], search_query=None, - filters={ - "timestamp": { - "date_max": [fecha_limite] - }}, + filters={}, exclude_filters = {} ) -- GitLab From 74384fd3e1de58a27d1ceefcead53a6cc4bf7644 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 17 Jul 2024 19:33:50 -0600 Subject: [PATCH 8/8] =?UTF-8?q?Filtros=20de=20fecha=20cambiados=20a=20gt?= =?UTF-8?q?=20y=20lt=20+=20solicitudes=20de=205=20meses=20m=C3=A1ximo=20re?= =?UTF-8?q?cuperadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 2 +- cosiap_api/solicitudes/views.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 62e2cf8..ae68c52 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -140,7 +140,7 @@ class DynamicTable(serializers.ModelSerializer): filter_info['lookups'] = ['gt', 'lt'] elif isinstance(field, models.DateField) or isinstance(field, models.DateTimeField): filter_info['html_type'] = 'dateInput' - filter_info['lookups'] = ['date_min', 'date_max'] + filter_info['lookups'] = ['gt', 'lt', 'gte', 'lte'] available_filters.append(filter_info) diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index dc5ce8c..b1ac5ca 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -11,8 +11,7 @@ from rest_framework.permissions import AllowAny from dynamic_tables.models import DynamicTableReport from .models import Solicitud from users.permisos import es_admin -from django.utils import timezone -from datetime import timedelta +from datetime import timedelta, datetime class SolicitudAPIView(BasePermissionAPIView): @@ -22,14 +21,18 @@ class SolicitudAPIView(BasePermissionAPIView): herencia: BasePermissionAPIView, clase con los permisos predeterminados para los usuarios ''' - permission_classes_list = [es_admin] - permission_classes_create = [es_admin] + permission_classes_list = [AllowAny] + permission_classes_create = [AllowAny] def get(self, request): ''' En la solicitud get manejamos la lógica para la lista de solicitudes mediante la tabla dinámica ''' + # Calculamos la fecha límite de 5 meses atrás desde hoy + fecha_limite = datetime.now() - timedelta(days=5*30) # Aproximadamente 5 meses + fecha_limite_str = fecha_limite.strftime('%Y-%m-%d') + # Construimos la configuración del reporte sin guardarlo en la base de datos (para que no se guarde cada vez que se hace una nueva solicitud) configuracion_reporte = DynamicTableReport( model_name='Solicitud', @@ -44,7 +47,11 @@ class SolicitudAPIView(BasePermissionAPIView): ], exclude_columns=[], search_query=None, - filters={}, + filters={ + 'timestamp': { + 'gte': [fecha_limite_str] + } + }, exclude_filters = {} ) -- GitLab