diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index ae68c528b57da5f4d335f07b1ed6435832c213c1..03a55928e43893b8a9349980caec719aca030eac 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -8,8 +8,10 @@ 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 +from django.db.models import ForeignKey, ManyToManyField, OneToOneField, ManyToOneRel, FileField, ImageField import re +from django.core.exceptions import ValidationError, FieldDoesNotExist +import json class DynamicTable(serializers.ModelSerializer): ''' @@ -21,6 +23,19 @@ class DynamicTable(serializers.ModelSerializer): model = DynamicTableReport fields = '__all__' + + def to_representation(self, instance): + ''' + Método to_representation: Devuelve la representación de la configuración de tabla dinámica excluyendo la data + + param: instance: instancia de configuración de tabla dinámica + ''' + representation = super().to_representation(instance) + # Eliminar el campo 'data' + representation.pop('data', None) + return representation + + def get_data(self, obj): ''' Clase para obtener los datos solicitados en la configuración de reporte enviada @@ -37,6 +52,11 @@ class DynamicTable(serializers.ModelSerializer): filters = obj.filters or {} exclude_filters = obj.exclude_filters or {} + # Obtener todas las columnas si se especifica '__all__' + if columns == '__all__': + columns = list(self.get_available_columns(obj).keys()) + obj.columns = columns + # Obtener los filtros disponibles para este modelo available_filters = self.get_available_filters(obj) @@ -101,12 +121,12 @@ class DynamicTable(serializers.ModelSerializer): ''' 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 con la configuración actual de la tabla dinámica ''' available_filters = [] - + model = self.buscar_modelo(obj) for column in obj.columns: @@ -118,11 +138,14 @@ class DynamicTable(serializers.ModelSerializer): field = field._meta.get_field(part).related_model else: field = field._meta.get_field(part) - label = field.verbose_name.capitalize() - + if hasattr(field, 'verbose_name'): + label = field.verbose_name.capitalize() + else: + label = part.capitalize() # Use field name as label if verbose_name is not available + filter_info = { 'campo': column, - 'label': label or field.verbose_name.capitalize(), + 'label': label, 'html_type': '', 'lookups': [], } @@ -141,9 +164,9 @@ class DynamicTable(serializers.ModelSerializer): elif isinstance(field, models.DateField) or isinstance(field, models.DateTimeField): filter_info['html_type'] = 'dateInput' filter_info['lookups'] = ['gt', 'lt', 'gte', 'lte'] - + available_filters.append(filter_info) - + return available_filters @@ -246,7 +269,7 @@ class DynamicTable(serializers.ModelSerializer): fields = {} # Expresión regular para excluir columnas específicas - exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at', re.IGNORECASE) + exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at|ptr|groups|permissions', re.IGNORECASE) for field in model._meta.get_fields(): # Verificar si el campo debe ser excluido @@ -312,7 +335,59 @@ class DynamicTable(serializers.ModelSerializer): queryset = queryset.filter(q_objects).exclude(exclude_q_objects) return queryset + + + def retrieve_instance_data(self, instance): + """ + Método para obtener todos los campos de la instancia y sus tablas relacionadas mediante foreign key. + :param instance: Instancia del modelo. + :return: Diccionario con todos los campos y sus valores. + """ + data = {} + try: + self.get_instance_fields(instance, data) + except Exception as e: + print(f"Error al obtener los campos de la instancia: {e}") + return data + + def get_instance_fields(self, instance, data): + """ + Método recursivo para obtener los campos de una instancia y sus relaciones. + + :param instance: Instancia del modelo. + :param data: Diccionario para almacenar los campos y sus valores. + """ + + # Expresión regular para excluir columnas específicas + exclude_pattern = re.compile(r'id|password|last_login|created_at|updated_at|ptr|groups|permissions', re.IGNORECASE) + + for field in instance._meta.get_fields(): + if exclude_pattern.search(field.name) or isinstance(field, ManyToOneRel): + continue + try: + field_value = getattr(instance, field.name, None) + + if isinstance(field, (ForeignKey, OneToOneField)): + if field_value: + related_data = {} + self.get_instance_fields(field_value, related_data) + data[field.name] = related_data + else: + data[field.name] = None + elif isinstance(field, ManyToManyField): + data[field.name] = list(field_value.values_list('pk', flat=True)) + elif isinstance(field, (FileField, ImageField)): + data[field.name] = field_value.url if field_value else None + else: + data[field.name] = field_value + except AttributeError as e: + print(f"Error al acceder al campo '{field.name}': {e}") + data[field.name] = None + except Exception as e: + print(f"Error general en el campo '{field.name}': {e}") + data[field.name] = str(e) + def create(self, validated_data): @@ -339,3 +414,118 @@ class DynamicTable(serializers.ModelSerializer): # Si no existe, creamos una nueva instancia instance = DynamicTableReport.objects.create(**validated_data) return instance + + + def update(self, instance, validated_data): + ''' + Método para modificar una configuración de reporte. + + param instance: Instancia de DynamicTableReport + param validated_data: Nuevos datos + ''' + instance.model_name = validated_data.get('model_name', instance.model_name) + instance.columns = validated_data.get('columns', instance.columns) + instance.exclude_columns = validated_data.get('exclude_columns', instance.exclude_columns) + instance.search_query = validated_data.get('search_query', instance.search_query) + instance.filters = validated_data.get('filters', instance.filters) + instance.exclude_filters = validated_data.get('exclude_filters', instance.exclude_filters) + + instance.save() + return instance + + + + def get_field(self, instance, field_name): + """ + Obtiene el campo del modelo para el campo especificado, ya sea en el modelo principal o en un modelo relacionado. + + :param instance: Instancia del modelo. + :param field_name: Nombre del campo, que puede ser en formato `campo__relacion`. + :return: Campo del modelo si existe, de lo contrario None. + """ + field_parts = field_name.split('__') + current_instance = instance + + for i, part in enumerate(field_parts): + try: + # Obtener el campo del modelo actual + field = current_instance._meta.get_field(part) + + # Si es un campo de relación, mover al siguiente nivel + if isinstance(field, (ForeignKey, OneToOneField)): + if i == len(field_parts) - 1: + return field + current_instance = getattr(current_instance, part) + else: + # El último campo en el path es el que queremos obtener + if i == len(field_parts) - 1: + return field + else: + raise ValueError(f"El campo '{part}' no es un campo de relación.") + except FieldDoesNotExist: + return None + + return None + + def update_field(self, instance, field_name, new_value): + """ + Actualiza el campo del modelo especificado, ya sea en el modelo principal o en un modelo relacionado. + + :param instance: Instancia del modelo. + :param field_name: Nombre del campo a actualizar, en formato `campo__relacion`. + :param new_value: Nuevo valor para el campo. + :return: True si se actualizó exitosamente, False de lo contrario. + """ + field_parts = field_name.split('__') + current_instance = instance + + for i, part in enumerate(field_parts): + try: + # Obtener el campo del modelo actual + field = current_instance._meta.get_field(part) + + if isinstance(field, (ForeignKey, OneToOneField)): + # Si es un campo de relación y no es el último, mover a la instancia relacionada + if i == len(field_parts) - 1: + setattr(current_instance, part, new_value) + current_instance.full_clean() # Validar antes de guardar + current_instance.save() + return True + current_instance = getattr(current_instance, part) + else: + # Actualizar el campo si es el último en el path + if i == len(field_parts) - 1: + setattr(current_instance, part, new_value) + current_instance.full_clean() # Validar antes de guardar + current_instance.save() + return True + else: + raise ValueError(f"El campo '{part}' no es un campo de relación.") + except (FieldDoesNotExist, AttributeError): + return False + except ValidationError as e: + return str(e) # Devolver el error de validación como string + + return False + + def update_fields(self, instance, field_updates): + """ + Actualiza múltiples campos del modelo especificado, ya sea en el modelo principal o en un modelo relacionado. + + :param instance: Instancia del modelo. + :param field_updates: Diccionario de campos a actualizar en el formato {"column1": "new_value", "column2": "new_value"}. + :return: True si todos los campos se actualizaron exitosamente, False de lo contrario. + """ + errors = {} + success = True + + for field_name, new_value in field_updates.items(): + result = self.update_field(instance, field_name, new_value) + if result is not True: + success = False + errors[field_name] = result + + if not success: + raise ValidationError(errors) # devolvemos un diccionario de errores surgidos durante las validaciones + + return success \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/tests.py b/cosiap_api/dynamic_tables/tests.py index 7ce503c2dd97ba78597f6ff6e4393132753573f6..dad5a5cce0dc4cbafa4f9a0dc1678e9dbb540774 100644 --- a/cosiap_api/dynamic_tables/tests.py +++ b/cosiap_api/dynamic_tables/tests.py @@ -1,3 +1,157 @@ 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 +from .models import DynamicTableReport -# Create your tests here. +class ReportesTests(TestCase): + ''' + Clase de prueba para las funcionalidades de reportes. + ''' + 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.is_staff = 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 instancias de DynamicTableReport + self.reporte1 = DynamicTableReport.objects.create( + model_name="Solicitud", + columns=[ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + exclude_columns=["monto_solicitado"], + filters={}, + exclude_filters={}, + search_query="" + ) + + self.reporte2 = DynamicTableReport.objects.create( + model_name="Solicitud", + columns=[ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + exclude_columns=["monto_aprobado"], + filters={}, + exclude_filters={}, + search_query="" + ) + + def test_get_reportes(self): + """ + Probar que se pueden recuperar todos los reportes con una solicitud GET + """ + url = reverse('dynamic-tables:reportes') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response.data, list) + self.assertEqual(len(response.data), 2) + + def test_get_reporte_por_pk(self): + """ + Probar que se puede recuperar un reporte por su pk con una solicitud GET + """ + url = reverse('dynamic-tables:reportes_pk', args=[self.reporte1.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['model_name'], 'Solicitud') + + def test_post_reporte(self): + """ + Probar la creación de un nuevo reporte con una solicitud POST + """ + url = reverse('dynamic-tables:reportes') + 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('success', response.data['messages']) + self.assertEqual(response.data['messages']['success'][0], 'Reporte creado con exito.') + + def test_put_reporte(self): + """ + Probar la actualización de un reporte con una solicitud PUT + """ + url = reverse('dynamic-tables:reportes_pk', args=[self.reporte1.pk]) + data = { + "model_name": "Solicitud", + "columns": [ + "status", + "solicitud_n", + "monto_solicitado", + "monto_aprobado", + "timestamp", + "solicitante__nombre" + ], + "exclude_columns": [ + "monto_aprobado" + ], + "filters": {}, + "exclude_filters": {}, + "search_query": "" + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('success', response.data['messages']) + self.assertEqual(response.data['messages']['success'][0], 'Reporte actualizado exitosamente.') + self.reporte1.refresh_from_db() + self.assertEqual(self.reporte1.exclude_columns, ["monto_aprobado"]) + + + def test_delete_reporte(self): + """ + Probar la eliminación de un reporte con una solicitud DELETE + """ + url = reverse('dynamic-tables:reportes_pk', args=[self.reporte1.pk]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(DynamicTableReport.objects.filter(pk=self.reporte1.pk).exists()) diff --git a/cosiap_api/dynamic_tables/urls.py b/cosiap_api/dynamic_tables/urls.py index c337d8687588acf093e43d3369957c09e096ffab..ff997cc13ea942373cfd1970cc4e09853e62aa47 100644 --- a/cosiap_api/dynamic_tables/urls.py +++ b/cosiap_api/dynamic_tables/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import DynamicTableView +from .views import ReporteAPIView app_name = 'dynamic-tables' urlpatterns = [ - path('', DynamicTableView.as_view(), name='dynamic_table'), -] \ No newline at end of file + path('', ReporteAPIView.as_view(), name='reportes'), + path('/', ReporteAPIView.as_view(), name='reportes_pk'), +] \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index b73cc09cd8da512c356454dbfe6e095eef27727e..1fc66327207fcb6348fd36306f6b7bf0fac5cc5f 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -1,19 +1,226 @@ -from rest_framework.views import APIView +from django.shortcuts import render, get_object_or_404 from rest_framework.response import Response from rest_framework import status -from .DynamicTable import DynamicTable +from dynamic_tables.DynamicTable import DynamicTable +from rest_framework.permissions import AllowAny, IsAuthenticated +from dynamic_tables.models import DynamicTableReport from users.views import BasePermissionAPIView +from users.permisos import es_admin +from datetime import timedelta, datetime +from notificaciones.mensajes import Mensaje +from django.core.exceptions import ValidationError +import json -class DynamicTableView(BasePermissionAPIView): +class DynamicTableAPIView(BasePermissionAPIView): ''' - Clase de APIView que heréda los permisos de la clase base - para el manejo de las tablas dinámicas y sus configuraciones + Clase abstracta para el manejo de tablas dinámicas ''' - def post(self, request): + permission_classes_update = [IsAuthenticated, es_admin] + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [IsAuthenticated, es_admin] + permission_classes_delete = [IsAuthenticated, es_admin] + + model_name = None + model_class = None + columns = '__all__' + exclude_columns = [] + search_query = "" + filters = {} + exclude_filters = {} + # Lista con los campos que no pueden ser modificados mediante la solicitud put + non_editable_fields = [] + + def get_configuracion_reporte(self, request): + ''' + Crear una configuración con los datos actuales de la request, sobrescribiendo los predeterminados si se proporcionan. + ''' + if request.method == 'GET': + # Extraemos el parámetro 'reporte' desde los parámetros de consulta + reporte_data = request.query_params.get('reporte', '{}') + try: + # Convertimos el string JSON en un diccionario + reporte_data = json.loads(reporte_data) + except ValueError: + reporte_data = {} + + elif request.method == 'PUT': + # Extraemos el parámetro 'reporte' desde los datos del cuerpo de la solicitud PUT + reporte_data = request.data.get('reporte', {}) + if isinstance(reporte_data, str): + try: + reporte_data = json.loads(reporte_data) + except ValueError: + reporte_data = {} + else: + reporte_data = {} + + # Verificamos si reporte_data es un diccionario vacío o None, asignamos configuración predeterminada + if not reporte_data: + reporte_data = { + 'model_name': self.model_name, + 'columns': self.columns, + 'filters': self.filters, + 'exclude_columns': self.exclude_columns, + 'search_query': self.search_query, + 'exclude_filters': self.exclude_filters + } + + # debemos actualizar las variables predeterminadas para el futuro uso de las mismas + self.model_name = reporte_data.get("model_name", self.model_name) + self.columns = reporte_data.get("columns", self.columns) + self.filters = reporte_data.get("filters", self.filters) + self.exclude_columns = reporte_data.get("exclude_columns", self.exclude_columns) + self.search_query = reporte_data.get("search_query", self.search_query) + self.exclude_filters = reporte_data.get("exclude_filters", self.exclude_filters) + return DynamicTableReport(**reporte_data) + + def get(self, request, pk=None): + + ''' + Método GET para obtener la lista de datos de acuerdo a la configuración + ''' + + response_data = {} + + if pk is not None: + # Recuperar la instancia y extraer los datos + instance = get_object_or_404(self.model_class, pk=pk) + serializer = DynamicTable() + instance_data = serializer.retrieve_instance_data(instance) + return Response(instance_data, status=status.HTTP_200_OK) + + configuracion_reporte = self.get_configuracion_reporte(request) + serializer = DynamicTable(instance=configuracion_reporte) + data = serializer.get_data(configuracion_reporte) + available_filters = serializer.get_available_filters(configuracion_reporte) + available_columns = serializer.get_available_columns(configuracion_reporte) + response_data = {'data': data, 'available_filters': available_filters, 'available_columns': available_columns} + return Response(response_data, status=status.HTTP_200_OK) + + + def put(self, request, pk, *args, **kwargs): + ''' + Permite a un admin modificar manualmente varias columnas de una fila en la tabla dinámica. + ''' + response_data = {} + # Obtener la instancia del modelo por el id proporcionado en la URL o un 404 en caso de no existir + instance = get_object_or_404(self.model_class, pk=pk) + field_updates = request.data.get('field_updates', {}) + + # extraemos la configuración, o si no fue enviada asignamos la predeterminada + configuracion = self.get_configuracion_reporte(request) + serializer = DynamicTable(instance=configuracion) + + # si tenemos un columns all + if self.columns == "__all__": + # debemos recuperar las columnas disponibles según la configuración + self.columns = list(serializer.get_available_columns(configuracion).keys()) + + # Verificamos que los campos estén disponibles en la tabla y que sean editables + for column_name in field_updates: + if column_name not in self.columns or column_name in self.non_editable_fields: + Mensaje.error(response_data, f"Modificación no permitida en la columna '{column_name}'.") + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + + try: + serializer = DynamicTable(instance=configuracion) + # Actualizar los valores de los campos y guardar la instancia + success = serializer.update_fields(instance, field_updates) + + if success: + Mensaje.success(response_data, 'Campos actualizados con éxito.') + return Response(response_data, status=status.HTTP_200_OK) + else: + Mensaje.error(response_data,'Ocurrió un error al actualizar algunos campos.') + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + + def delete(self, request, pk, *args, **kwargs): + ''' + Método para elminar un registro de la tabla dinámica + + ''' + + response_data = {} + modelo_eliminar = self.model_class + + instance = get_object_or_404(modelo_eliminar, pk=pk) + instance.delete() + + Mensaje.success(response_data, 'Registro eliminado con éxito.') + return Response(response_data, status=status.HTTP_204_NO_CONTENT) + + + +class ReporteAPIView(BasePermissionAPIView): + ''' + Clase para manejar la lógica de las configuraciónes de reportes en tablas dinámicas + ''' + + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [IsAuthenticated, es_admin] + permission_classes_update = [IsAuthenticated, es_admin] + permission_classes_delete = [IsAuthenticated, es_admin] + + + def get(self, request, *args, **kwargs): + ''' + Método GET para obtener una lista de reportes de tabla dinámica + o un reporte en caso de recibir un pk + ''' + if 'pk' in kwargs: + instance = get_object_or_404(DynamicTableReport, pk=kwargs['pk']) + serializer = DynamicTable(instance) + return Response(serializer.data) + + queryset = DynamicTableReport.objects.all() + serializer = DynamicTable(queryset, many=True) + return Response(serializer.data) + + def post(self, request, *args, **kwargs): + ''' + Método POST para crear una nueva configuración de reporte + ''' + response_data = {} serializer = DynamicTable(data=request.data) if serializer.is_valid(): - instance = serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + configuracion_reporte = serializer.save() + Mensaje.success(response_data, 'Reporte creado con exito.') + return Response(response_data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def put(self, request, pk, *args, **kwargs): + ''' + Método put para la actualización de una configuración de reporte. + + param pk: llave primaria del reporte a editar + ''' + response_data = {} + configuracion = get_object_or_404(DynamicTableReport, pk= pk) + + serializer = DynamicTable(instance=configuracion, data=request.data) + if serializer.is_valid(): + # guardamos los cambios + serializer.save() + Mensaje.success(response_data, 'Reporte actualizado exitosamente.') + return Response(response_data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def delete(self, request, pk, *args, **kwargs): + ''' + Método delete para eliminar una configuración de reporte + + param pk: llave primaria del reporte a eliminar + ''' + response_data = {} + configuracion = get_object_or_404(DynamicTableReport, pk= pk) + configuracion.delete() + + Mensaje.success(response_data, 'Reporte eliminado con éxito.') + return Response(response_data, status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index afcaf94a862be71cf2a7f71ea6c0c8703dd5554a..1c5644e2934eb2be9678004f7fc782d221530ca2 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -1,9 +1,8 @@ -# 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 users.models import Usuario, Solicitante, Municipio from .models import Solicitud from django.utils import timezone from common import custom_tests as c_tests @@ -28,9 +27,9 @@ class PermisosSolicitudTests(c_tests.PermissionTestCase): class SolicitudTests(TestCase): - ''' + """ Clase de prueba de la lista de solicitudes usando DynamicTable - ''' + """ def setUp(self): """Configurar el entorno de prueba""" @@ -61,11 +60,6 @@ class SolicitudTests(TestCase): 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 Solicitante self.solicitante = Solicitante.objects.create( curp="CEVA020423HGRRZDA9", @@ -77,10 +71,11 @@ class SolicitudTests(TestCase): RFC="CEVA0204237E4", direccion="Calle Falsa 123", codigo_postal="12345", - municipio_id= 2, + municipio= Municipio.objects.get(id=1), poblacion="Test Poblacion", datos_bancarios=None, # Asignar datos bancarios si es necesario - INE=None # Asignar archivo de INE si es necesario + INE=None, # Asignar archivo de INE si es necesario + password="password" ) # Crear instancias de Solicitud usando la instancia de Solicitante @@ -91,7 +86,6 @@ class SolicitudTests(TestCase): monto_solicitado=1000, monto_aprobado=800, observacion="Observación 1", - # Asignar otros campos necesarios ) self.solicitud2 = Solicitud.objects.create( @@ -101,24 +95,23 @@ class SolicitudTests(TestCase): 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') + url = reverse('solicitudes:solicitudes') 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): + def test_get_solicitudes_exclusion_columnas(self): """ - Probar la exclusión de columnas con una solicitud POST + Probar la exclusión de columnas con una solicitud GET """ - url = reverse('solicitudes:solicitud-list') + url = reverse('solicitudes:solicitudes') data = { "model_name": "Solicitud", "columns": [ @@ -136,16 +129,17 @@ class SolicitudTests(TestCase): "exclude_filters": {}, "search_query": "" } - response = self.client.post(url, data, format='json') + query_params = {'reporte': json.dumps(data)} + response = self.client.get(url, query_params, 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): + def test_get_solicitudes_con_filtros(self): """ - Probar la aplicación de filtros con una solicitud POST + Probar la aplicación de filtros con una solicitud GET """ - url = reverse('solicitudes:solicitud-list') + url = reverse('solicitudes:solicitudes') data = { "model_name": "Solicitud", "columns": [ @@ -165,18 +159,18 @@ class SolicitudTests(TestCase): "exclude_filters": {}, "search_query": "" } - response = self.client.post(url, data, format='json') + query_params = {'reporte': json.dumps(data)} + response = self.client.get(url, query_params, 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_or(self): + def test_get_solicitudes_con_filtros_or(self): """ - Probar la aplicación de filtros OR con una solicitud POST + Probar la aplicación de filtros OR con una solicitud GET """ - url = reverse('solicitudes:solicitud-list') + url = reverse('solicitudes:solicitudes') data = { "model_name": "Solicitud", "columns": [ @@ -199,17 +193,18 @@ class SolicitudTests(TestCase): "exclude_filters": {}, "search_query": "" } - response = self.client.post(url, data, format='json') + query_params = {'reporte': json.dumps(data)} + response = self.client.get(url, query_params, 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): + def test_get_solicitudes_con_filtros_exclusion(self): """ - Probar la aplicación de filtros de exclusión con una solicitud POST + Probar la aplicación de filtros de exclusión con una solicitud GET """ - url = reverse('solicitudes:solicitud-list') + url = reverse('solicitudes:solicitudes') data = { "model_name": "Solicitud", "columns": [ @@ -229,17 +224,18 @@ class SolicitudTests(TestCase): }, "search_query": "" } - response = self.client.post(url, data, format='json') + query_params = {'reporte': json.dumps(data)} + response = self.client.get(url, query_params, 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): + def test_get_solicitudes_con_search_query(self): """ - Probar la aplicación de un search_query con una solicitud POST + Probar la aplicación de un search_query con una solicitud GET """ - url = reverse('solicitudes:solicitud-list') + url = reverse('solicitudes:solicitudes') data = { "model_name": "Solicitud", "columns": [ @@ -255,11 +251,79 @@ class SolicitudTests(TestCase): "exclude_filters": {}, "search_query": "Adalberto" } - response = self.client.post(url, data, format='json') + query_params = {'reporte': json.dumps(data)} + response = self.client.get(url, query_params, 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') + def test_put_solicitud_modificacion_valida(self): + """ + Probar la modificación válida de una columna con una solicitud PUT + """ + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) + data = { + "field_updates":{ + "solicitante__nombre": "AdalCerrillo", + "solicitante__telefono": "4920000000" + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.solicitud1.refresh_from_db() + self.assertEqual(self.solicitud1.solicitante.telefono, "4920000000") + self.assertEqual(self.solicitud1.solicitante.nombre, "AdalCerrillo") - + def test_put_solicitud_modificacion_columna_no_editable(self): + """ + Probar la modificación de una columna que no es editable con una solicitud PUT + """ + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) + data = { + "field_updates":{ + "status": "Rechazado" + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_put_datos_incorrectos(self): + """ + Probar las validaciones al momento de editar una columna + """ + + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) + data = { + "field_updates":{ + "solicitante__telefono": "4900000000000000000000" + } + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_solicitud(self): + """ + Probar la eliminación de una solicitud con una solicitud DELETE + """ + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_detalles_solicitud(self): + """ + Probar la recuperación completa de todos los campos de una solicitud + """ + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data + + # Verificar los campos en el nivel superior + self.assertIn('status', data) + self.assertEqual(data['status'], 'Pendiente') + + # Verificar los campos dentro del diccionario 'solicitante' + solicitante_data = data.get('solicitante', {}) + self.assertIn('nombre', solicitante_data) + self.assertEqual(solicitante_data['nombre'], self.solicitante.nombre) \ No newline at end of file diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index 2e64de1cf6dc6b25c88ea37040c0e29e0f5df901..53e9b00d392e7f248067062bedfce9a53781f3fa 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -5,5 +5,6 @@ from django.contrib.auth import views as auth_views app_name = 'solicitudes' urlpatterns = [ - path('', views.SolicitudAPIView.as_view(), name='solicitud-list'), + path('', views.SolicitudAPIView.as_view(), name='solicitudes'), + path('/', views.SolicitudAPIView.as_view(), name='solicitudes_pk'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index b1ac5ca1fb39a149246125db0dc00750a99d1605..2b38431c61a0f0eb02d13ca1eaa5d6f008b73997 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -2,100 +2,25 @@ # 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 rest_framework.permissions import AllowAny -from dynamic_tables.models import DynamicTableReport +from dynamic_tables.views import DynamicTableAPIView from .models import Solicitud from users.permisos import es_admin +from rest_framework.permissions import IsAuthenticated from datetime import timedelta, datetime -class SolicitudAPIView(BasePermissionAPIView): - ''' +class SolicitudAPIView(DynamicTableAPIView): + ''' 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_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', - 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] - } - }, - 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) - - # 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, 'available_columns': available_columns} - - # Devolvemos la respuesta - return Response(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 - ''' - # 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 - configuracion_reporte = serializer.save() - - # Recupera los datos según la configuración - data = serializer.get_data(configuracion_reporte) - - # 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, 'available_columns': available_columns} - - return Response(response_data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + model_class = Solicitud + model_name = 'Solicitud' + columns = '__all__' + exclude_columns = [] + filters = { + 'timestamp': { + 'gte': [(datetime.now() - timedelta(days=5*30)).strftime('%Y-%m-%d')] + } + } + non_editable_fields = ["status"] \ No newline at end of file diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index e51266956fd44862a24f8fb53dae6ab32efccc74..3d6b01c2069ba9432795b14e04e5bf9ffe2eb797 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -151,7 +151,7 @@ class Solicitante(Usuario): # campo de relacion uno a uno con los datos bancarios datos_bancarios = models.OneToOneField(DatosBancarios, verbose_name="Datos Bancarios",null=True, blank=True, on_delete=models.CASCADE ) # campo para la identificación oficial - INE = models.FileField(verbose_name='INE', upload_to= nombre_archivo_ine , null=True, blank=False) + INE = models.FileField(verbose_name='INE', upload_to= nombre_archivo_ine , null=True, blank=True) def __str__(self): return self.nombre diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index abb1b72285d356a49a11a313aaadde97bda63e73..f3cb8348ae8abf48882c417042c05d85a91ca785 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -74,9 +74,8 @@ class UsuarioAPIView(BasePermissionAPIView): - BasePermissionAPIView (Heréda de la clase con los permisos predefinidos) """ permission_classes_create = [AllowAny] - permission_classes_delete = [es_admin] - permission_classes_list = [es_admin] - permission_classes_update = [IsAuthenticated, primer_login] + permission_classes_delete = [IsAuthenticated, es_admin] + permission_classes_list = [IsAuthenticated, es_admin] # Función para la obtención de la lista de usuarios def get( self, request, *args, **kwargs ): @@ -167,8 +166,9 @@ class SolicitanteAPIView(BasePermissionAPIView): - BasePermissionAPIView (Heréda de la clase con los permisos predefinidos) """ # Indicamos que solo los usuarios logeados puedan acceder a esta función - permission_classes = [IsAuthenticated, primer_login] - permission_classes_create = [IsAuthenticated] + permission_classes_create = [IsAuthenticated, primer_login] + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_update = [IsAuthenticated, primer_login] # Petición get para listar a los solicitantes def get(self, request, *args, **kwargs):