From 0cb0026f668fb097f13ea9bc05478ef5361f79ed Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 22 Jul 2024 13:30:18 -0600 Subject: [PATCH 1/4] =?UTF-8?q?Primera=20parte=20de=20edici=C3=B3n=20de=20?= =?UTF-8?q?casillas=20tabla=20din=C3=A1mica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 76 ++++++++++++++- cosiap_api/dynamic_tables/urls.py | 6 +- cosiap_api/dynamic_tables/views.py | 88 +++++++++++++++-- cosiap_api/solicitudes/tests.py | 41 +++++--- cosiap_api/solicitudes/urls.py | 1 + cosiap_api/solicitudes/views.py | 114 +++++----------------- 6 files changed, 213 insertions(+), 113 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index ae68c52..623aaf4 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -8,8 +8,9 @@ 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 import re +from django.core.exceptions import ValidationError, FieldDoesNotExist class DynamicTable(serializers.ModelSerializer): ''' @@ -339,3 +340,76 @@ class DynamicTable(serializers.ModelSerializer): # Si no existe, creamos una nueva instancia instance = DynamicTableReport.objects.create(**validated_data) 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.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.save() + return True + else: + raise ValueError(f"El campo '{part}' no es un campo de relación.") + except FieldDoesNotExist: + return False + except AttributeError: + return False + + return False diff --git a/cosiap_api/dynamic_tables/urls.py b/cosiap_api/dynamic_tables/urls.py index c337d86..83265a8 100644 --- a/cosiap_api/dynamic_tables/urls.py +++ b/cosiap_api/dynamic_tables/urls.py @@ -1,7 +1,7 @@ from django.urls import path -from .views import DynamicTableView +from .views import DynamicTableAPIView app_name = 'dynamic-tables' urlpatterns = [ - path('', DynamicTableView.as_view(), name='dynamic_table'), -] \ No newline at end of file + path('', DynamicTableAPIView.as_view(), name='dynamic_table'), +] \ No newline at end of file diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index b73cc09..31c713a 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -1,19 +1,89 @@ -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 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 -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): + model_name = None + model_class = None + columns = [] + exclude_columns = [] + default_filters = {} + # Lista con los campos que no pueden ser modificados mediane la solicitud put + non_editable_fields = ['id', 'status'] + response_data = {} + + def get_configuracion_reporte(self): + ''' + Método para obtener la configuración del reporte desde la solicitud + ''' + return DynamicTableReport( + model_name=self.model_name, + columns=self.columns, + exclude_columns=self.exclude_columns, + search_query=None, + filters=self.default_filters, + exclude_filters={} + ) + + def get(self, request): + ''' + Método GET para obtener la lista de datos de acuerdo a la configuración + ''' + configuracion_reporte = self.get_configuracion_reporte() + 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 post(self, request, *args, **kwargs): + ''' + Método POST para manejar los filtros, exclusión de columnas y búsqueda + ''' 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() + 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) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def put(self, request, pk, *args, **kwargs): + ''' + Permite a un admin modificar manualmente una columna específica de una fila en la tabla dinámica. + ''' + # 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) + + column_name = request.data.get('column') + new_value = request.data.get('value') + + # Verificamos que el campo esté disponible en la tabla y que sea editable + if column_name not in self.columns or column_name in self.non_editable_fields: + Mensaje.error(self.response_data, "Modificación no permitida en esta columna.") + return Response(self.response_data, status=status.HTTP_400_BAD_REQUEST) + + try: + # Actualizar el valor del campo y guardar la instancia + serializer = DynamicTable(instance= self.get_configuracion_reporte()) + serializer.update_field(instance, column_name, new_value) + return Response({'message': 'Campo actualizado con éxito.'}, status=status.HTTP_200_OK) + except ValidationError as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 5cf35fe..8a3b6f4 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -42,16 +42,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 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", @@ -63,7 +53,7 @@ class SolicitudTests(TestCase): RFC="CEVA0204237E4", direccion="Calle Falsa 123", codigo_postal="12345", - municipio= self.municipio, + 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 @@ -248,4 +238,33 @@ class SolicitudTests(TestCase): 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:solicitud-get-update', args=[self.solicitud1.pk]) + data = { + "column": "solicitante__telefono", + "value": "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") + + def test_put_solicitud_modificacion_no_permitida(self): + """ + Probar que no se permite la modificación de columnas no permitidas con una solicitud PUT + """ + url = reverse('solicitudes:solicitud-get-update', args=[self.solicitud1.pk]) + data = { + "column": "status", + "value": "Rechazado" + } + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.solicitud1.refresh_from_db() + self.assertNotEqual(self.solicitud1.status, "Rechazado") + + diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index 2e64de1..ac4484e 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -6,4 +6,5 @@ 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='solicitud-get-update'), ] \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index b1ac5ca..d47684a 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -2,100 +2,36 @@ # 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 + + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_create = [IsAuthenticated, es_admin] + + 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 = [] + default_filters = { + 'timestamp': { + 'gte': [(datetime.now() - timedelta(days=5*30)).strftime('%Y-%m-%d')] + } + } \ No newline at end of file -- GitLab From 6452766ec7b1a4a39e3c0fa3b8badcc083ed4225 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Tue, 23 Jul 2024 19:38:50 -0600 Subject: [PATCH 2/4] =?UTF-8?q?adaptaci=C3=B3n=20del=20m=C3=A9todo=20get?= =?UTF-8?q?=20de=20DynamicTable,=20validaci=C3=B3nes=20al=20editar=20colum?= =?UTF-8?q?na?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 54 +++++++-- cosiap_api/dynamic_tables/views.py | 135 +++++++++++++++------- cosiap_api/solicitudes/tests.py | 99 +++++++++------- cosiap_api/solicitudes/urls.py | 4 +- cosiap_api/solicitudes/views.py | 15 +-- cosiap_api/users/models.py | 2 +- 6 files changed, 199 insertions(+), 110 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index 623aaf4..e7e55ef 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -38,6 +38,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) @@ -102,12 +107,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: @@ -119,11 +124,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': [], } @@ -142,9 +150,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 @@ -342,6 +350,7 @@ class DynamicTable(serializers.ModelSerializer): 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. @@ -374,7 +383,6 @@ class DynamicTable(serializers.ModelSerializer): 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. @@ -396,6 +404,7 @@ class DynamicTable(serializers.ModelSerializer): # 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) @@ -403,13 +412,36 @@ class DynamicTable(serializers.ModelSerializer): # 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: - return False - except AttributeError: + 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 diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index 31c713a..c2aa3f7 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -2,12 +2,14 @@ from django.shortcuts import render, get_object_or_404 from rest_framework.response import Response from rest_framework import status from dynamic_tables.DynamicTable import DynamicTable +from rest_framework.permissions import AllowAny 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 DynamicTableAPIView(BasePermissionAPIView): @@ -15,75 +17,124 @@ class DynamicTableAPIView(BasePermissionAPIView): Clase abstracta para el manejo de tablas dinámicas ''' + permission_classes_update = [AllowAny] + model_name = None model_class = None - columns = [] + columns = '__all__' exclude_columns = [] - default_filters = {} - # Lista con los campos que no pueden ser modificados mediane la solicitud put - non_editable_fields = ['id', 'status'] + search_query = "" + filters = {} + exclude_filters = {} + # Lista con los campos que no pueden ser modificados mediante la solicitud put + non_editable_fields = [] response_data = {} - def get_configuracion_reporte(self): + def get_configuracion_reporte(self, request): ''' - Método para obtener la configuración del reporte desde la solicitud + Crear una configuración con los datos actuales de la request, sobrescribiendo los predeterminados si se proporcionan. ''' - return DynamicTableReport( - model_name=self.model_name, - columns=self.columns, - exclude_columns=self.exclude_columns, - search_query=None, - filters=self.default_filters, - exclude_filters={} - ) + 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 + } + + 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): ''' Método GET para obtener la lista de datos de acuerdo a la configuración ''' - configuracion_reporte = self.get_configuracion_reporte() + 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 post(self, request, *args, **kwargs): - ''' - Método POST para manejar los filtros, exclusión de columnas y búsqueda - ''' - serializer = DynamicTable(data=request.data) - if serializer.is_valid(): - configuracion_reporte = serializer.save() - 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) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def put(self, request, pk, *args, **kwargs): ''' - Permite a un admin modificar manualmente una columna específica de una fila en la tabla dinámica. + Permite a un admin modificar manualmente varias columnas de una fila en la tabla dinámica. ''' # 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', {}) - column_name = request.data.get('column') - new_value = request.data.get('value') + # extraemos la configuración, o si no fue enviada asignamos la predeterminada + configuracion = self.get_configuracion_reporte(request) + serializer = DynamicTable(instance=configuracion) - # Verificamos que el campo esté disponible en la tabla y que sea editable - if column_name not in self.columns or column_name in self.non_editable_fields: - Mensaje.error(self.response_data, "Modificación no permitida en esta columna.") - return Response(self.response_data, status=status.HTTP_400_BAD_REQUEST) + # 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(self.response_data, f"Modificación no permitida en la columna '{column_name}'.") + return Response(self.response_data, status=status.HTTP_400_BAD_REQUEST) try: - # Actualizar el valor del campo y guardar la instancia - serializer = DynamicTable(instance= self.get_configuracion_reporte()) - serializer.update_field(instance, column_name, new_value) - return Response({'message': 'Campo actualizado con éxito.'}, status=status.HTTP_200_OK) + serializer = DynamicTable(instance=configuracion) + # Actualizar los valores de los campos y guardar la instancia + success = serializer.update_fields(instance, field_updates) + + if success: + return Response({'message': 'Campos actualizados con éxito.'}, status=status.HTTP_200_OK) + else: + return Response({'error': 'Ocurrió un error al actualizar algunos campos.'}, status=status.HTTP_400_BAD_REQUEST) except ValidationError as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class ReporteAPIView(BasePermissionAPIView): + ''' + Clase para manejar la lógica de las configuraciónes de reportes en tablas dinámicas + ''' + + def post(self, request, *args, **kwargs): + ''' + Método POST para crear una nueva configuración de reporte + ''' + serializer = DynamicTable(data=request.data) + if serializer.is_valid(): + 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) \ No newline at end of file diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 8a3b6f4..57a6c78 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -1,17 +1,15 @@ -# 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 - +import json class SolicitudTests(TestCase): - ''' + """ Clase de prueba de la lista de solicitudes usando DynamicTable - ''' + """ def setUp(self): """Configurar el entorno de prueba""" @@ -56,7 +54,8 @@ class SolicitudTests(TestCase): 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 @@ -67,7 +66,6 @@ class SolicitudTests(TestCase): monto_solicitado=1000, monto_aprobado=800, observacion="Observación 1", - # Asignar otros campos necesarios ) self.solicitud2 = Solicitud.objects.create( @@ -77,24 +75,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": [ @@ -112,16 +109,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": [ @@ -141,18 +139,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": [ @@ -175,17 +173,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": [ @@ -205,17 +204,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": [ @@ -231,40 +231,53 @@ 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:solicitud-get-update', args=[self.solicitud1.pk]) + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) data = { - "column": "solicitante__telefono", - "value": "4920000000" + "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_no_permitida(self): + def test_put_solicitud_modificacion_columna_no_editable(self): """ - Probar que no se permite la modificación de columnas no permitidas con una solicitud PUT + Probar la modificación de una columna que no es editable con una solicitud PUT """ - url = reverse('solicitudes:solicitud-get-update', args=[self.solicitud1.pk]) + url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) data = { - "column": "status", - "value": "Rechazado" + "field_updates":{ + "status": "Rechazado" + } } response = self.client.put(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.solicitud1.refresh_from_db() - self.assertNotEqual(self.solicitud1.status, "Rechazado") + 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) \ No newline at end of file diff --git a/cosiap_api/solicitudes/urls.py b/cosiap_api/solicitudes/urls.py index ac4484e..53e9b00 100644 --- a/cosiap_api/solicitudes/urls.py +++ b/cosiap_api/solicitudes/urls.py @@ -5,6 +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='solicitud-get-update'), + 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 d47684a..9e8035b 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -20,18 +20,11 @@ class SolicitudAPIView(DynamicTableAPIView): permission_classes_create = [IsAuthenticated, es_admin] 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' - ] + columns = '__all__' exclude_columns = [] - default_filters = { + filters = { 'timestamp': { 'gte': [(datetime.now() - timedelta(days=5*30)).strftime('%Y-%m-%d')] } - } \ No newline at end of file + } + 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 e512669..3d6b01c 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 -- GitLab From 4539dd0aef26c29870e7ef179b31ecb48ac7154a Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 24 Jul 2024 12:50:53 -0600 Subject: [PATCH 3/4] Clase ReporteAPIView completa, test incluidos --- cosiap_api/dynamic_tables/DynamicTable.py | 37 ++++- cosiap_api/dynamic_tables/tests.py | 156 +++++++++++++++++++++- cosiap_api/dynamic_tables/urls.py | 5 +- cosiap_api/dynamic_tables/views.py | 94 +++++++++++-- cosiap_api/solicitudes/tests.py | 10 +- cosiap_api/solicitudes/views.py | 4 - cosiap_api/users/views.py | 10 +- 7 files changed, 292 insertions(+), 24 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index e7e55ef..f5a34df 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -22,6 +22,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 @@ -255,7 +268,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 @@ -349,7 +362,25 @@ class DynamicTable(serializers.ModelSerializer): 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): """ @@ -444,4 +475,4 @@ class DynamicTable(serializers.ModelSerializer): if not success: raise ValidationError(errors) # devolvemos un diccionario de errores surgidos durante las validaciones - return success + 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 7ce503c..dad5a5c 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 83265a8..ff997cc 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 DynamicTableAPIView +from .views import ReporteAPIView app_name = 'dynamic-tables' urlpatterns = [ - path('', DynamicTableAPIView.as_view(), name='dynamic_table'), + 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 c2aa3f7..76789ba 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -2,7 +2,7 @@ from django.shortcuts import render, get_object_or_404 from rest_framework.response import Response from rest_framework import status from dynamic_tables.DynamicTable import DynamicTable -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from dynamic_tables.models import DynamicTableReport from users.views import BasePermissionAPIView from users.permisos import es_admin @@ -17,7 +17,10 @@ class DynamicTableAPIView(BasePermissionAPIView): Clase abstracta para el manejo de tablas dinámicas ''' - permission_classes_update = [AllowAny] + 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 @@ -28,7 +31,6 @@ class DynamicTableAPIView(BasePermissionAPIView): exclude_filters = {} # Lista con los campos que no pueden ser modificados mediante la solicitud put non_editable_fields = [] - response_data = {} def get_configuracion_reporte(self, request): ''' @@ -65,6 +67,7 @@ class DynamicTableAPIView(BasePermissionAPIView): '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) @@ -74,9 +77,12 @@ class DynamicTableAPIView(BasePermissionAPIView): return DynamicTableReport(**reporte_data) def get(self, request): + ''' Método GET para obtener la lista de datos de acuerdo a la configuración ''' + + response_data = {} configuracion_reporte = self.get_configuracion_reporte(request) serializer = DynamicTable(instance=configuracion_reporte) data = serializer.get_data(configuracion_reporte) @@ -90,6 +96,7 @@ class DynamicTableAPIView(BasePermissionAPIView): ''' 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', {}) @@ -106,8 +113,8 @@ class DynamicTableAPIView(BasePermissionAPIView): # 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(self.response_data, f"Modificación no permitida en la columna '{column_name}'.") - return Response(self.response_data, status=status.HTTP_400_BAD_REQUEST) + 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) @@ -115,26 +122,97 @@ class DynamicTableAPIView(BasePermissionAPIView): success = serializer.update_fields(instance, field_updates) if success: - return Response({'message': 'Campos actualizados con éxito.'}, status=status.HTTP_200_OK) + Mensaje.success(response_data, 'Campos actualizados con éxito.') + return Response(response_data, status=status.HTTP_200_OK) else: - return Response({'error': 'Ocurrió un error al actualizar algunos campos.'}, status=status.HTTP_400_BAD_REQUEST) + 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(): 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) \ No newline at end of file + 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 57a6c78..6e901fe 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -280,4 +280,12 @@ class SolicitudTests(TestCase): } } response = self.client.put(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) \ No newline at end of file + 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) \ No newline at end of file diff --git a/cosiap_api/solicitudes/views.py b/cosiap_api/solicitudes/views.py index 9e8035b..2b38431 100644 --- a/cosiap_api/solicitudes/views.py +++ b/cosiap_api/solicitudes/views.py @@ -15,10 +15,6 @@ class SolicitudAPIView(DynamicTableAPIView): ''' model_class = Solicitud - - permission_classes_list = [IsAuthenticated, es_admin] - permission_classes_create = [IsAuthenticated, es_admin] - model_name = 'Solicitud' columns = '__all__' exclude_columns = [] diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index abb1b72..f3cb834 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): -- GitLab From c8b28786420b065764c5569cb57e7374c0e65ef0 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 24 Jul 2024 19:19:35 -0600 Subject: [PATCH 4/4] =?UTF-8?q?Detalle=20de=20un=20registro=20desde=20tabl?= =?UTF-8?q?a=20din=C3=A1mica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/dynamic_tables/DynamicTable.py | 55 ++++++++++++++++++++++- cosiap_api/dynamic_tables/views.py | 10 ++++- cosiap_api/solicitudes/tests.py | 20 ++++++++- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/cosiap_api/dynamic_tables/DynamicTable.py b/cosiap_api/dynamic_tables/DynamicTable.py index f5a34df..03a5592 100644 --- a/cosiap_api/dynamic_tables/DynamicTable.py +++ b/cosiap_api/dynamic_tables/DynamicTable.py @@ -8,9 +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, OneToOneField, 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): ''' @@ -334,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): diff --git a/cosiap_api/dynamic_tables/views.py b/cosiap_api/dynamic_tables/views.py index 76789ba..1fc6632 100644 --- a/cosiap_api/dynamic_tables/views.py +++ b/cosiap_api/dynamic_tables/views.py @@ -76,13 +76,21 @@ class DynamicTableAPIView(BasePermissionAPIView): self.exclude_filters = reporte_data.get("exclude_filters", self.exclude_filters) return DynamicTableReport(**reporte_data) - def get(self, request): + 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) diff --git a/cosiap_api/solicitudes/tests.py b/cosiap_api/solicitudes/tests.py index 6e901fe..7432dac 100644 --- a/cosiap_api/solicitudes/tests.py +++ b/cosiap_api/solicitudes/tests.py @@ -288,4 +288,22 @@ class SolicitudTests(TestCase): """ url = reverse('solicitudes:solicitudes_pk', args=[self.solicitud1.pk]) response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) \ No newline at end of file + 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 -- GitLab